feat: Implement bank system, actor interaction UI, and notification system with corresponding backend logic and update gitignore.

This commit is contained in:
Jacob Schmidt 2026-02-17 21:18:17 -06:00
parent 2dbcb98817
commit 6c8490f299
23 changed files with 1640 additions and 2748 deletions

3
.gitignore vendored
View File

@ -24,3 +24,6 @@ target/
# OS
.DS_Store
Thumbs.db
# Arma
arma/ui/map-viewer/

View File

@ -4,7 +4,7 @@
* File: fnc_handleUIEvents.sqf
* Author: IDSolutions
* Date: 2026-01-28
* Last Update: 2026-02-06
* Last Update: 2026-02-17
* Public: No
*
* Description:
@ -32,6 +32,7 @@ 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::close::menu": { closeDialog 1; };
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."; };

View File

@ -4,7 +4,7 @@
* File: fnc_initActorClass.sqf
* Author: IDSolutions
* Date: 2026-01-28
* Last Update: 2026-02-13
* Last Update: 2026-02-17
* Public: Yes
*
* Description:
@ -118,6 +118,7 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [
{
private _storeType = _x getVariable ["storeType", ""];
private _isAtm = _x getVariable ["isAtm", false];
private _isBank = _x getVariable ["isBank", false];
private _isGarage = _x getVariable ["isGarage", false];
private _isLocker = _x getVariable ["isLocker", false];
@ -126,6 +127,7 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [
private _isPlayer = _x isKindOf "Man" && isPlayer _x;
if (_storeType isNotEqualTo "") then { _nearbyActions pushBack ["store", _storeType]; };
if (_isAtm) then { _nearbyActions pushBack ["atm", true]; };
if (_isBank) then { _nearbyActions pushBack ["bank", true]; };
if (_isLocker && GVAR(enableVA)) then { _nearbyActions pushBack ["va", true]; };
if (_isGarage) then { _nearbyActions pushBack ["garage", _garageType]; };

View File

@ -1,11 +1,11 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interaction Menu</title>
<link rel="stylesheet" href="style.css" />
<!-- <link rel="stylesheet" href="style.css"> -->
<!--
Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API
@ -32,39 +32,8 @@
</head>
<body>
<div class="container">
<div class="neu-menu">
<div class="neu-menu-content">
<div class="neu-menu-grid" id="menuGrid"></div>
</div>
</div>
</div>
<script src="script.js"></script>
<script>
function updateState() {
if (typeof store !== "undefined") {
const state = store.getState();
const menuGrid = document.getElementById("menuGrid");
if (state.menuItems.length === 0) {
if (menuGrid) menuGrid.style.display = "none";
} else {
if (menuGrid) menuGrid.style.display = "grid";
}
}
}
setTimeout(() => {
if (typeof store !== "undefined") {
store.subscribe((state) => {
updateState();
});
updateState();
}
}, 1000);
</script>
<div id="app"></div>
<!-- <script src="script.js"></script> -->
</body>
</html>

View File

@ -1,12 +1,66 @@
/**
* Redux-like Pattern for Actor Menu Management
* Interaction Menu - Modern UI Implementation
* Uses vanilla JS with React-like patterns and Redux-like state management
*/
//=============================================================================
// #region LIBRARY - DOM Helper & State Management
//=============================================================================
// Helper to create DOM elements (React-like createElement)
function h(tag, props = {}, ...children) {
const el = document.createElement(tag);
if (props) {
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith('on') && typeof value === 'function') {
el.addEventListener(key.substring(2).toLowerCase(), value);
} else if (key === 'className') {
el.className = value;
} else if (key === 'style' && typeof value === 'object') {
Object.assign(el.style, value);
} else {
el.setAttribute(key, value);
}
});
}
children.forEach(child => {
if (typeof child === 'string' || typeof child === 'number') {
el.appendChild(document.createTextNode(child));
} else if (child instanceof Node) {
el.appendChild(child);
} else if (Array.isArray(child)) {
child.forEach(c => {
if (c instanceof Node) el.appendChild(c);
});
}
});
return el;
}
// Simple Rendering Logic
let _rootContainer = null;
let _rootComponent = null;
function render(component, container) {
_rootContainer = container;
_rootComponent = component;
_render();
}
function _render() {
if (_rootContainer && _rootComponent) {
_rootContainer.innerHTML = '';
_rootContainer.appendChild(_rootComponent());
}
}
//=============================================================================
// #region ACTIONS
//=============================================================================
// Action Types
const ActionTypes = {
SET_AVAILABLE_ACTIONS: "SET_AVAILABLE_ACTIONS",
SET_MENU_ITEMS: "SET_MENU_ITEMS",
@ -15,7 +69,6 @@ const ActionTypes = {
CLEAR_ACTIONS: "CLEAR_ACTIONS",
};
// Action Creators
const actions = {
setAvailableActions: (actionTypes) => ({
type: ActionTypes.SET_AVAILABLE_ACTIONS,
@ -47,84 +100,91 @@ const actions = {
//=============================================================================
const baseMenuItems = [
{
id: "atm",
title: "ATM",
description: "Access the ATM",
icon: "",
action: "actor::open::atm",
},
{
id: "bank",
title: "Banking Services",
description: "Access your bank account and manage finances",
icon: "",
action: "actor::open::bank",
},
{
id: "phone",
title: "Personal Phone",
title: "Phone",
description: "Access and manage your personal phone",
icon: "",
action: "actor::open::phone",
},
{
id: "org",
title: "Organization Dashboard",
title: "Organization",
description: "View and manage your organization data",
icon: "",
action: "actor::open::org",
},
{
id: "store",
title: "Store",
description: "Browse and purchase items from the store",
icon: "",
action: "actor::open::store",
},
];
const actionDefinitions = {
atm: {
id: "atm",
title: "ATM",
description: "Access the ATM",
action: "actor::open::atm",
},
bank: {
id: "bank",
title: "Bank",
description: "Access your bank account and manage finances",
action: "actor::open::bank",
},
phone: {
id: "phone",
title: "Phone",
description: "Access and manage your personal phone",
action: "actor::open::phone",
},
org: {
id: "org",
title: "Organization",
description: "View and manage your organization data",
action: "actor::open::org",
},
store: {
id: "store",
title: "Store",
description: "Browse and purchase items from the store",
action: "actor::open::store",
},
device: {
id: "device",
title: "Device Interaction",
title: "Device",
description: "Manage devices and settings",
icon: "",
action: "actor::open::device",
},
garage: {
id: "garage",
title: "Vehicle Garage",
title: "Garage",
description: "Access and manage your vehicle collection",
icon: "",
action: "actor::open::garage",
},
player: {
id: "player",
title: "Player Interaction",
title: "Player",
description: "Interact with player-specific actions",
icon: "",
action: "actor::open::iplayer",
},
store: {
id: "store",
title: "Store",
description: "Browse and purchase items from the store",
icon: "",
action: "actor::open::store",
},
va: {
id: "va",
title: "Virtual Arsenal",
title: "Arsenal",
description: "Access your virtual arsenal",
icon: "",
action: "actor::open::vlocker",
},
vg: {
id: "vg",
title: "Virtual Garage",
title: "V. Garage",
description: "Access your virtual garage",
icon: "",
action: "actor::open::vgarage",
},
};
@ -141,7 +201,6 @@ function actorReducer(state = initialState, action) {
case ActionTypes.SET_AVAILABLE_ACTIONS:
const newMenuItems = [...state.baseMenuItems];
// Process available actions
const actionArray = Array.isArray(action.payload)
? action.payload
: [];
@ -152,9 +211,7 @@ function actorReducer(state = initialState, action) {
if (definition) {
newMenuItems.push(definition);
} else {
console.warn(
`No definition found for: ${type} - ${value}`,
);
console.warn(`No definition found for: ${type} - ${value}`);
}
} else {
console.warn("Invalid action format:", actionItem);
@ -175,10 +232,7 @@ function actorReducer(state = initialState, action) {
case ActionTypes.ADD_ACTION:
const definition = state.actionDefinitions[action.payload];
if (
definition &&
!state.menuItems.find((item) => item.id === definition.id)
) {
if (definition && !state.menuItems.find((item) => item.id === definition.id)) {
return {
...state,
menuItems: [...state.menuItems, definition],
@ -189,9 +243,7 @@ function actorReducer(state = initialState, action) {
case ActionTypes.REMOVE_ACTION:
return {
...state,
menuItems: state.menuItems.filter(
(item) => item.id !== action.payload,
),
menuItems: state.menuItems.filter((item) => item.id !== action.payload),
};
case ActionTypes.CLEAR_ACTIONS:
@ -225,6 +277,7 @@ class Store {
console.log("Dispatching action:", action);
this.state = this.reducer(this.state, action);
this.listeners.forEach((listener) => listener(this.state));
_render(); // Re-render on state change
}
subscribe(listener) {
@ -235,7 +288,6 @@ class Store {
}
}
// Create store instance
const store = new Store(actorReducer, initialState);
//=============================================================================
@ -247,100 +299,141 @@ const selectors = {
getAvailableActions: (state) => state.availableActions,
getBaseMenuItems: (state) => state.baseMenuItems,
getActionDefinitions: (state) => state.actionDefinitions,
getMenuItemById: (state, id) =>
state.menuItems.find((item) => item.id === id),
getMenuItemById: (state, id) => state.menuItems.find((item) => item.id === id),
getMenuItemsCount: (state) => state.menuItems.length,
};
//=============================================================================
// #region UI COMPONENTS (Redux-connected)
// #region UI COMPONENTS
//=============================================================================
class ActorUI {
constructor(store) {
this.store = store;
this.unsubscribe = null;
// Tooltip state
let tooltipEl = null;
function createTooltip() {
if (!tooltipEl) {
tooltipEl = h('div', { className: 'radial-tooltip' },
h('div', { className: 'tooltip-title' }),
h('div', { className: 'tooltip-description' })
);
document.body.appendChild(tooltipEl);
}
return tooltipEl;
}
init() {
console.log("ActorUI initializing...");
function showTooltip(item, x, y) {
const tooltip = createTooltip();
tooltip.querySelector('.tooltip-title').textContent = item.title;
tooltip.querySelector('.tooltip-description').textContent = item.description;
tooltip.style.left = `${x + 15}px`;
tooltip.style.top = `${y + 10}px`;
tooltip.classList.add('visible');
}
// Subscribe to state changes
this.unsubscribe = this.store.subscribe((state) => {
this.render(state);
});
// Initial render
this.render(this.store.getState());
// Request initial data
this.requestInitialData();
console.log("ActorUI initialized successfully");
function hideTooltip() {
if (tooltipEl) {
tooltipEl.classList.remove('visible');
}
}
render(state) {
this.updateMenuDisplay(state);
}
function RadialItem({ item, index, total, onClick }) {
const menuRadius = 160;
const itemSize = 80;
updateMenuDisplay(state) {
const grid = document.getElementById("menuGrid");
if (!grid) {
console.error("Menu grid element not found");
return;
// Calculate position in circle
const angleStep = (2 * Math.PI) / total;
const angle = angleStep * index - Math.PI / 2; // Start from top
const centerX = menuRadius + itemSize / 2;
const centerY = menuRadius + itemSize / 2;
const x = centerX + menuRadius * Math.cos(angle) - itemSize / 2;
const y = centerY + menuRadius * Math.sin(angle) - itemSize / 2;
const el = h('div', {
className: 'radial-item',
style: {
left: `${x}px`,
top: `${y}px`
},
onClick: () => onClick(item)
},
h('div', { className: 'radial-item-title' }, item.title)
);
// Add tooltip events
el.addEventListener('mouseenter', (e) => showTooltip(item, e.clientX, e.clientY));
el.addEventListener('mousemove', (e) => {
if (tooltipEl && tooltipEl.classList.contains('visible')) {
tooltipEl.style.left = `${e.clientX + 15}px`;
tooltipEl.style.top = `${e.clientY + 10}px`;
}
});
el.addEventListener('mouseleave', hideTooltip);
// Clear existing menu items
grid.innerHTML = "";
return el;
}
// Render menu items
const menuItems = selectors.getMenuItems(state);
menuItems.forEach((item) => {
const menuItem = document.createElement("div");
menuItem.className = "neu-menu-item";
menuItem.setAttribute("data-action", item.action);
menuItem.innerHTML = `
<div class="neu-menu-item-icon">${item.icon}</div>
<div class="neu-menu-item-title">${item.title}</div>
<div class="neu-menu-item-description">${item.description}</div>
`;
menuItem.addEventListener("click", () =>
this.handleMenuItemClick(item),
);
function RadialCenter({ onClose }) {
return h('div', {
className: 'radial-center',
onClick: onClose
},
h('div', { className: 'center-label' }, 'Close')
);
}
grid.appendChild(menuItem);
});
function RadialMenu() {
const state = store.getState();
const menuItems = selectors.getMenuItems(state);
console.log(`Rendered ${menuItems.length} menu items`);
}
handleMenuItemClick(item) {
const handleItemClick = (item) => {
console.log("Menu item clicked:", item);
const alert = {
event: item.action,
data: {},
};
A3API.SendAlert(JSON.stringify(alert));
}
if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify(alert));
}
};
requestInitialData() {
console.log("Requesting initial actor data...");
const handleClose = () => {
console.log("Close menu requested");
const alert = {
event: "actor::get::actions",
event: "actor::close::menu",
data: {},
};
A3API.SendAlert(JSON.stringify(alert));
if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify(alert));
}
};
if (menuItems.length === 0) {
return h('div', { className: 'empty-state' },
h('p', null, 'No actions available')
);
}
destroy() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
return h('div', { className: 'radial-menu' },
RadialCenter({ onClose: handleClose }),
menuItems.map((item, index) =>
RadialItem({
item,
index,
total: menuItems.length,
onClick: handleItemClick
})
)
);
}
function App() {
return RadialMenu();
}
//=============================================================================
// #region DATA HANDLERS (Redux-connected)
// #region DATA HANDLERS (A3API Integration)
//=============================================================================
function updateAvailableActions(actionTypes) {
@ -354,78 +447,45 @@ function handleGetActionsResponse(data) {
}
//=============================================================================
// #region ACTION HANDLERS
// #region INITIALIZATION
//=============================================================================
function handleMenuItemClick(item) {
console.log("Legacy menu item click handler:", item);
const alert = {
event: item.action,
data: {},
};
A3API.SendAlert(JSON.stringify(alert));
}
let initialized = false;
//=============================================================================
// #region INITIALIZATION FUNCTIONS
//=============================================================================
// Global flag to prevent double initialization
let actorUIInitialized = false;
/**
* Initialize the actor interface - called from HTML after script loads
*/
function initializeMenu() {
console.log("initializeMenu() called");
if (actorUIInitialized) {
console.log("ActorUI already initialized, skipping...");
if (initialized) {
console.log("Menu already initialized, skipping...");
return;
}
// Check if DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
if (!actorUIInitialized) {
console.log("DOM loaded, initializing ActorUI...");
window.actorUI = new ActorUI(store);
window.actorUI.init();
actorUIInitialized = true;
}
});
const root = document.getElementById('app');
if (root) {
render(App, root);
initialized = true;
console.log("Interaction menu initialized successfully");
// Request initial data from A3API
if (typeof A3API !== 'undefined') {
const alert = {
event: "actor::get::actions",
data: {},
};
A3API.SendAlert(JSON.stringify(alert));
}
} else {
// DOM is already ready
console.log("DOM already ready, initializing ActorUI...");
window.actorUI = new ActorUI(store);
window.actorUI.init();
actorUIInitialized = true;
console.error("Root element #app not found");
}
}
//=============================================================================
// #region GLOBAL VARIABLES
//=============================================================================
// Make actorUI globally accessible
let actorUI;
// Auto-initialize if DOM is already loaded when script executes
// Auto-initialize based on DOM state
if (document.readyState !== "loading") {
console.log("Script loaded after DOM ready, auto-initializing...");
if (!actorUIInitialized) {
actorUI = new ActorUI(store);
actorUI.init();
actorUIInitialized = true;
}
initializeMenu();
} else {
// Wait for DOM to be ready
document.addEventListener("DOMContentLoaded", () => {
if (!actorUIInitialized) {
console.log("DOM loaded, initializing ActorUI...");
actorUI = new ActorUI(store);
actorUI.init();
actorUIInitialized = true;
}
console.log("DOM loaded, initializing menu...");
initializeMenu();
});
}

View File

@ -1,417 +0,0 @@
/**
* Redux-like Pattern for Actor Menu Management
*/
//=============================================================================
// #region ACTIONS
//=============================================================================
// Action Types
const ActionTypes = {
SET_AVAILABLE_ACTIONS: "SET_AVAILABLE_ACTIONS",
SET_MENU_ITEMS: "SET_MENU_ITEMS",
ADD_ACTION: "ADD_ACTION",
REMOVE_ACTION: "REMOVE_ACTION",
CLEAR_ACTIONS: "CLEAR_ACTIONS",
};
// Action Creators
const actions = {
setAvailableActions: (actionTypes) => ({
type: ActionTypes.SET_AVAILABLE_ACTIONS,
payload: actionTypes,
}),
setMenuItems: (menuItems) => ({
type: ActionTypes.SET_MENU_ITEMS,
payload: menuItems,
}),
addAction: (actionType) => ({
type: ActionTypes.ADD_ACTION,
payload: actionType,
}),
removeAction: (actionType) => ({
type: ActionTypes.REMOVE_ACTION,
payload: actionType,
}),
clearActions: () => ({
type: ActionTypes.CLEAR_ACTIONS,
}),
};
//=============================================================================
// #region REDUCER
//=============================================================================
const baseMenuItems = [
{
id: "bank",
title: "Banking Services",
description: "Access your bank account and manage finances",
icon: "",
action: "actor::open::bank",
},
{
id: "phone",
title: "Personal Phone",
description: "Access and manage your personal phone",
icon: "",
action: "actor::open::phone",
},
];
const actionDefinitions = {
device: {
id: "device",
title: "Device Interaction",
description: "Manage devices and settings",
icon: "",
action: "actor::open::device",
},
garage: {
id: "garage",
title: "Vehicle Garage",
description: "Access and manage your vehicle collection",
icon: "",
action: "actor::open::garage",
},
locker: {
id: "locker",
title: "Locker",
description: "Access your personal locker for storage",
icon: "",
action: "actor::open::locker",
},
player: {
id: "player",
title: "Player Interaction",
description: "Interact with player-specific actions",
icon: "",
action: "actor::open::iplayer",
},
store: {
id: "store",
title: "Store",
description: "Browse and purchase items from the store",
icon: "",
action: "actor::open::store",
},
va: {
id: "va",
title: "Virtual Arsenal",
description: "Access your virtual arsenal",
icon: "",
action: "actor::open::arsenal",
},
vg: {
id: "vg",
title: "Virtual Garage",
description: "Access your virtual garage",
icon: "",
action: "actor::open::vgarage",
},
};
const initialState = {
availableActions: [],
menuItems: [...baseMenuItems],
baseMenuItems: [...baseMenuItems],
actionDefinitions: { ...actionDefinitions },
};
function actorReducer(state = initialState, action) {
switch (action.type) {
case ActionTypes.SET_AVAILABLE_ACTIONS:
const newMenuItems = [...state.baseMenuItems];
// Process available actions
const actionArray = Array.isArray(action.payload)
? action.payload
: [];
actionArray.forEach((actionItem) => {
if (Array.isArray(actionItem) && actionItem.length === 2) {
const [type, value] = actionItem;
const definition = state.actionDefinitions[value];
if (definition) {
newMenuItems.push(definition);
} else {
console.warn(
`No definition found for: ${type} - ${value}`,
);
}
} else {
console.warn("Invalid action format:", actionItem);
}
});
return {
...state,
availableActions: action.payload,
menuItems: newMenuItems,
};
case ActionTypes.SET_MENU_ITEMS:
return {
...state,
menuItems: action.payload,
};
case ActionTypes.ADD_ACTION:
const definition = state.actionDefinitions[action.payload];
if (
definition &&
!state.menuItems.find((item) => item.id === definition.id)
) {
return {
...state,
menuItems: [...state.menuItems, definition],
};
}
return state;
case ActionTypes.REMOVE_ACTION:
return {
...state,
menuItems: state.menuItems.filter(
(item) => item.id !== action.payload,
),
};
case ActionTypes.CLEAR_ACTIONS:
return {
...state,
availableActions: [],
menuItems: [...state.baseMenuItems],
};
default:
return state;
}
}
//=============================================================================
// #region STORE
//=============================================================================
class Store {
constructor(reducer, initialState) {
this.reducer = reducer;
this.state = initialState;
this.listeners = [];
}
getState() {
return this.state;
}
dispatch(action) {
console.log("Dispatching action:", action);
this.state = this.reducer(this.state, action);
this.listeners.forEach((listener) => listener(this.state));
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter((l) => l !== listener);
};
}
}
// Create store instance
const store = new Store(actorReducer, initialState);
//=============================================================================
// #region SELECTORS
//=============================================================================
const selectors = {
getMenuItems: (state) => state.menuItems,
getAvailableActions: (state) => state.availableActions,
getBaseMenuItems: (state) => state.baseMenuItems,
getActionDefinitions: (state) => state.actionDefinitions,
getMenuItemById: (state, id) =>
state.menuItems.find((item) => item.id === id),
getMenuItemsCount: (state) => state.menuItems.length,
};
//=============================================================================
// #region UI COMPONENTS (Redux-connected)
//=============================================================================
class ActorUI {
constructor(store) {
this.store = store;
this.unsubscribe = null;
}
init() {
console.log("ActorUI initializing...");
// Subscribe to state changes
this.unsubscribe = this.store.subscribe((state) => {
this.render(state);
});
// Initial render
this.render(this.store.getState());
// Request initial data
this.requestInitialData();
console.log("ActorUI initialized successfully");
}
render(state) {
this.updateMenuDisplay(state);
}
updateMenuDisplay(state) {
const grid = document.getElementById("menuGrid");
if (!grid) {
console.error("Menu grid element not found");
return;
}
// Clear existing menu items
grid.innerHTML = "";
// Render menu items
const menuItems = selectors.getMenuItems(state);
menuItems.forEach((item) => {
const menuItem = document.createElement("div");
menuItem.className = "neu-menu-item";
menuItem.setAttribute("data-action", item.action);
menuItem.innerHTML = `
<div class="neu-menu-item-icon">${item.icon}</div>
<div class="neu-menu-item-title">${item.title}</div>
<div class="neu-menu-item-description">${item.description}</div>
`;
menuItem.addEventListener("click", () =>
this.handleMenuItemClick(item),
);
grid.appendChild(menuItem);
});
console.log(`Rendered ${menuItems.length} menu items`);
}
handleMenuItemClick(item) {
console.log("Menu item clicked:", item);
const alert = {
event: item.action,
data: {},
};
A3API.SendAlert(JSON.stringify(alert));
}
requestInitialData() {
console.log("Requesting initial actor data...");
const alert = {
event: "actor::get::actions",
data: {},
};
A3API.SendAlert(JSON.stringify(alert));
}
destroy() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
}
//=============================================================================
// #region DATA HANDLERS (Redux-connected)
//=============================================================================
function updateAvailableActions(actionTypes) {
console.log("Updating available actions:", actionTypes);
store.dispatch(actions.setAvailableActions(actionTypes));
}
function handleGetActionsResponse(data) {
console.log("Received actions data:", data);
store.dispatch(actions.setAvailableActions(data));
}
//=============================================================================
// #region ACTION HANDLERS
//=============================================================================
function handleMenuItemClick(item) {
console.log("Legacy menu item click handler:", item);
const alert = {
event: item.action,
data: {},
};
A3API.SendAlert(JSON.stringify(alert));
}
//=============================================================================
// #region INITIALIZATION FUNCTIONS
//=============================================================================
// Global flag to prevent double initialization
let actorUIInitialized = false;
/**
* Initialize the actor interface - called from HTML after script loads
*/
function initializeMenu() {
console.log("initializeMenu() called");
if (actorUIInitialized) {
console.log("ActorUI already initialized, skipping...");
return;
}
// Check if DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
if (!actorUIInitialized) {
console.log("DOM loaded, initializing ActorUI...");
window.actorUI = new ActorUI(store);
window.actorUI.init();
actorUIInitialized = true;
}
});
} else {
// DOM is already ready
console.log("DOM already ready, initializing ActorUI...");
window.actorUI = new ActorUI(store);
window.actorUI.init();
actorUIInitialized = true;
}
}
//=============================================================================
// #region GLOBAL VARIABLES
//=============================================================================
// Make actorUI globally accessible
let actorUI;
// Auto-initialize if DOM is already loaded when script executes
if (document.readyState !== "loading") {
console.log("Script loaded after DOM ready, auto-initializing...");
if (!actorUIInitialized) {
actorUI = new ActorUI(store);
actorUI.init();
actorUIInitialized = true;
}
} else {
// Wait for DOM to be ready
document.addEventListener("DOMContentLoaded", () => {
if (!actorUIInitialized) {
console.log("DOM loaded, initializing ActorUI...");
actorUI = new ActorUI(store);
actorUI.init();
actorUIInitialized = true;
}
});
}

View File

@ -1,3 +1,20 @@
:root {
--bg-app: rgba(0, 0, 0, 0.4);
--bg-surface: #ffffff;
--bg-surface-hover: #f1f5f9;
--primary: #475569;
--primary-hover: #1e293b;
--text-main: #1f2937;
--text-muted: #64748b;
--text-inverse: #f8fafc;
--border: #e2e8f0;
--radius: 8px;
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--menu-radius: 160px;
--item-size: 80px;
}
* {
margin: 0;
padding: 0;
@ -5,112 +22,164 @@
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
height: 100vh;
width: 100vw;
background: rgba(0, 0, 0, 0.5);
font-family: Arial, sans-serif;
background: var(--bg-app);
color: var(--text-main);
line-height: 1.4;
overflow: hidden;
}
.container {
align-items: flex-end;
#app {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
/* Radial Menu Container */
.radial-menu {
position: relative;
width: calc(var(--menu-radius) * 2 + var(--item-size));
height: calc(var(--menu-radius) * 2 + var(--item-size));
}
/* Center Hub */
.radial-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90px;
height: 90px;
background: var(--bg-surface);
border: 2px solid var(--border);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding-right: 5%;
perspective: 1200px;
}
box-shadow: var(--shadow-lg);
z-index: 10;
cursor: pointer;
transition: all 0.2s ease;
.neu-menu {
background: rgba(15, 20, 30, 0.9);
border: 1px solid rgba(100, 150, 200, 0.4);
border-radius: 4px;
margin-right: 25%;
max-height: 640px;
width: 480px;
transform: rotateY(-10deg) translateZ(0);
transform-style: preserve-3d;
box-shadow:
-5px 0 15px rgba(100, 150, 200, 0.2),
0 8px 32px rgba(0, 0, 0, 0.8);
&:hover {
background: var(--bg-surface-hover);
border-color: var(--primary);
transform: translate(-50%, -50%) scale(1.05);
}
.neu-menu-content {
height: 100%;
overflow: hidden;
padding: 1rem;
.center-icon {
font-size: 1.25rem;
margin-bottom: 0.15rem;
}
.neu-menu-grid {
display: grid;
max-height: 380px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
-webkit-scrollbar-width: thin;
.neu-menu-item {
align-items: flex-start;
background: rgba(20, 30, 45, 0.7);
border-left: 3px solid rgba(100, 150, 200, 0.5);
border-radius: 2px;
color: rgba(200, 220, 240, 0.95);
display: flex;
flex-direction: column;
justify-content: center;
margin-bottom: 0.5rem;
min-height: 70px;
padding: 0.75rem 1rem;
text-align: left;
transition: all 0.15s ease;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 3px;
background: rgba(100, 150, 200, 0.8);
opacity: 0;
transition: opacity 0.15s ease;
}
&:last-child {
margin-bottom: 0 !important;
}
&:hover {
background: rgba(30, 45, 70, 0.9);
border-left-color: rgba(150, 200, 255, 0.9);
box-shadow:
0 0 20px rgba(100, 150, 200, 0.2),
inset 0 0 30px rgba(100, 150, 200, 0.05);
cursor: pointer;
&::before {
opacity: 1;
}
}
.neu-menu-item-description {
color: rgba(140, 160, 180, 0.85);
font-size: 0.8rem;
line-height: 1.3;
margin-top: 0.35rem;
}
.neu-menu-item-icon {
display: none;
}
.neu-menu-item-title {
color: rgba(200, 220, 255, 1);
font-size: 1rem;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
}
}
.center-label {
font-size: 0.65rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
}
/* Menu Items */
.radial-item {
position: absolute;
width: var(--item-size);
height: var(--item-size);
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: var(--shadow);
text-align: center;
&:hover {
background: var(--bg-surface-hover);
border-color: var(--primary);
transform: scale(1.15);
box-shadow: var(--shadow-lg);
z-index: 5;
.radial-item-title {
color: var(--primary-hover);
}
}
&:active {
transform: scale(0.95);
}
}
.radial-item-icon {
font-size: 1.25rem;
margin-bottom: 0.25rem;
}
.radial-item-title {
font-size: 0.6rem;
font-weight: 600;
color: var(--text-main);
line-height: 1.2;
transition: color 0.2s ease;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* Tooltip */
.radial-tooltip {
position: fixed;
background: var(--primary-hover);
color: var(--text-inverse);
padding: 0.5rem 0.75rem;
border-radius: var(--radius);
font-size: 0.75rem;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s ease;
z-index: 100;
box-shadow: var(--shadow-lg);
&.visible {
opacity: 1;
}
.tooltip-title {
font-weight: 600;
}
.tooltip-description {
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.7);
margin-top: 0.15rem;
}
}
/* Empty state */
.empty-state {
text-align: center;
padding: 2rem;
color: var(--text-muted);
background: var(--bg-surface);
border-radius: var(--radius);
box-shadow: var(--shadow);
p {
font-size: 0.9rem;
}
}

View File

@ -1,186 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-color: #3b82f6;
--primary-hover: #2563eb;
--secondary-color: #1e293b;
--background-color: rgba(15, 23, 42, 0.85);
--card-background: rgba(30, 41, 59, 0.95);
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--border-color: #334155;
--success-color: #16a34a;
--success-hover: #15803d;
--button-hover: #4f46e5;
}
body {
font-family:
"Inter",
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
line-height: 1.6;
background-color: transparent;
color: var(--text-primary);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.menu-container {
background-color: var(--background-color);
border-radius: 16px;
width: 90%;
max-width: 800px;
backdrop-filter: blur(10px);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
overflow: hidden;
.menu-header {
background-color: var(--secondary-color);
padding: 1.5rem;
border-bottom: 1px solid var(--border-color);
margin-bottom: 20px;
h1 {
color: var(--text-primary);
font-size: 1.5rem;
font-weight: 600;
letter-spacing: -0.025em;
margin: 0;
}
}
.menu-content {
padding: 1.5rem;
.menu-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
}
}
.menu-item {
background-color: var(--card-background);
border-radius: 12px;
padding: 1.25rem;
transition: all 0.3s ease;
cursor: pointer;
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
animation: fadeIn 0.3s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
border-color: var(--primary-color);
}
.menu-item-icon {
font-size: 2rem;
margin-bottom: 1rem;
background: linear-gradient(
135deg,
var(--primary-color),
var(--button-hover)
);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
svg {
width: 24px;
height: 24px;
stroke: currentColor;
fill: none;
}
}
.menu-item-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.menu-item-description {
font-size: 0.875rem;
color: var(--text-secondary);
line-height: 1.4;
}
}
.loading-state {
text-align: center;
padding: 40px 20px;
color: var(--text-secondary);
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(248, 250, 252, 0.1);
border-top: 4px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
.loading-text {
font-size: 16px;
color: var(--text-secondary);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 640px) {
.menu-container {
width: 95%;
margin: 1rem;
.menu-content {
.menu-grid {
grid-template-columns: 1fr;
}
}
.menu-header {
h1 {
font-size: 1.25rem;
}
}
}
}

View File

@ -4,7 +4,7 @@
* File: fnc_handleUIEvents.sqf
* Author: IDSolutions
* Date: 2025-12-16
* Last Update: 2026-02-13
* Last Update: 2026-02-17
* Public: No
*
* Description:
@ -32,6 +32,7 @@ private _uid = GVAR(BankClass) get "uid";
private _account = GVAR(BankClass) get "account";
private _bank = _account get "bank";
private _cash = _account get "cash";
private _earnings = _account get "earnings";
private _pin = _account get "pin";
private _funds = EGVAR(org,OrgClass) get "funds";
@ -45,8 +46,9 @@ switch (_event) do {
private _players = SREG(bank,IndexRegistry);
private _accountData = createHashMapFromArray [
["uid", _uid],
["cash", _cash],
["bank", _bank],
["cash", _cash],
["earnings", _earnings],
["org", _funds],
["pin", _pin],
["players", _players]
@ -85,6 +87,12 @@ switch (_event) do {
[SRPC(bank,requestTransfer), [_uid, _target, _from, _amount]] call CFUNC(serverEvent);
};
case "bank::depositEarnings": {
private _amount = _data get "amount";
if (_amount > _earnings) exitWith { hint "Insufficient earnings!"; };
[SRPC(bank,requestDepositEarnings), [_uid, _amount]] call CFUNC(serverEvent);
};
case "bank::close": { closeDialog 1; };
// ========================================================================

View File

@ -1,585 +1,188 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
:root {
--bg-app: #fdfcf8;
--bg-surface: #ffffff;
--bg-surface-hover: #f1f5f9;
--primary: #475569;
--primary-hover: #1e293b;
--text-main: #1f2937;
--text-muted: #64748b;
--text-inverse: #f8fafc;
--border: #e2e8f0;
--radius: 8px;
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--footer-bg: #1e293b;
}
body {
height: 100vh;
width: 100vw;
background: rgba(0, 0, 0, 0.5);
font-family: Arial, sans-serif;
color: rgba(200, 220, 240, 0.95);
overflow: hidden;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
margin: 0;
padding: 0;
background: transparent;
color: var(--text-main);
line-height: 1.6;
}
.atm-container {
height: 100vh;
width: 100vw;
#app {
min-height: 100vh;
}
main {
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 5%;
perspective: 1200px;
flex-direction: column;
min-height: 100vh;
padding: 3rem 0;
box-sizing: border-box;
}
.atm-screen {
width: 480px;
height: 640px;
background: rgba(15, 20, 30, 0.95);
border: 2px solid rgba(100, 150, 200, 0.5);
border-radius: 8px;
transform: rotateY(-10deg) translateZ(0);
transform-style: preserve-3d;
box-shadow:
-8px 0 20px rgba(100, 150, 200, 0.25),
0 8px 32px rgba(0, 0, 0, 0.8);
display: grid;
grid-template-rows: auto 1fr auto;
overflow: hidden;
margin-right: 25%;
.container {
max-width: 800px;
width: 100%;
background: #f1f5f9;
margin: 0 auto;
padding: 2rem;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
}
/* Header */
.atm-header {
padding: 1.25rem 1.5rem;
background: rgba(20, 30, 45, 0.9);
border-bottom: 2px solid rgba(100, 150, 200, 0.3);
display: flex;
align-items: center;
gap: 1rem;
}
.atm-logo {
font-size: 2rem;
}
.atm-title {
font-size: 1rem;
font-weight: 600;
letter-spacing: 1px;
text-transform: uppercase;
color: rgba(100, 150, 200, 1);
}
/* Content */
.atm-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
overflow-x: hidden;
}
.atm-content::-webkit-scrollbar {
width: 6px;
}
.atm-content::-webkit-scrollbar-track {
background: rgba(15, 20, 30, 0.5);
}
.atm-content::-webkit-scrollbar-thumb {
background: rgba(100, 150, 200, 0.3);
border-radius: 3px;
}
.atm-view {
display: flex;
flex-direction: column;
gap: 1.5rem;
height: 100%;
justify-content: space-between;
}
.atm-view h3 {
font-size: 1.125rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(200, 220, 255, 1);
.header {
text-align: center;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(100, 150, 200, 0.2);
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--border);
h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
letter-spacing: -0.025em;
color: var(--primary-hover);
}
p {
color: var(--text-muted);
font-size: 1.1rem;
margin: 0;
}
}
/* Welcome Screen */
.welcome-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 2rem 1rem;
flex: 1;
}
.welcome-icon {
font-size: 4rem;
opacity: 0.6;
}
.welcome-message h2 {
font-size: 1.5rem;
font-weight: 600;
color: rgba(200, 220, 255, 1);
}
.welcome-message p {
font-size: 0.875rem;
color: rgba(140, 160, 180, 0.85);
}
/* PIN Entry */
.pin-entry {
display: flex;
flex-direction: column;
gap: 1.5rem;
flex: 1;
justify-content: center;
}
.pin-entry h3 {
margin: 0;
padding: 0;
border: none;
/* Cards */
.card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem;
margin-bottom: 2rem;
box-shadow: var(--shadow);
text-align: center;
h2 {
margin-top: 0;
font-size: 1.8rem;
color: var(--primary-hover);
}
}
/* PIN Display */
.pin-display {
display: flex;
justify-content: center;
gap: 1rem;
padding: 1.5rem;
font-size: 2.5rem;
letter-spacing: 0.5rem;
text-align: center;
margin-bottom: 2rem;
font-family: monospace;
color: var(--primary);
}
.pin-dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: rgba(100, 150, 200, 0.2);
border: 2px solid rgba(100, 150, 200, 0.4);
transition: all 0.2s ease;
}
.pin-dot.filled {
background: rgba(100, 150, 200, 0.8);
border-color: rgba(150, 200, 255, 0.8);
box-shadow: 0 0 10px rgba(100, 150, 200, 0.5);
}
.keypad {
/* Numpad */
.numpad {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
.key-btn {
padding: 1rem;
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: 1.125rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
}
.key-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);
}
.key-btn:active {
transform: scale(0.95);
}
.key-clear {
background: rgba(200, 150, 100, 0.2);
border-color: rgba(200, 150, 100, 0.4);
}
.key-clear:hover {
background: rgba(200, 150, 100, 0.3);
border-color: rgba(255, 200, 150, 0.6);
}
.key-enter {
background: rgba(100, 150, 200, 0.2);
border-color: rgba(100, 150, 200, 0.5);
}
.key-enter:hover {
background: rgba(100, 150, 200, 0.3);
border-color: rgba(150, 200, 255, 0.7);
}
/* Account Summary */
.account-summary {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
flex-shrink: 0;
max-width: 300px;
margin: 0 auto;
button {
padding: 1.5rem;
font-size: 1.5rem;
background: var(--bg-surface);
color: var(--text-main);
border: 1px solid var(--border);
box-shadow: var(--shadow);
margin: 0;
&:hover {
background: var(--primary);
color: white;
border-color: var(--primary);
}
}
}
.summary-item {
padding: 1.25rem;
background: rgba(20, 30, 45, 0.6);
border: 1px solid rgba(100, 150, 200, 0.3);
border-left: 3px solid rgba(100, 150, 200, 0.5);
border-radius: 4px;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.summary-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(140, 160, 180, 0.85);
}
.summary-value {
font-size: 1.25rem;
font-weight: 600;
color: rgba(100, 200, 150, 1);
}
/* Menu Options */
.menu-options {
display: grid;
grid-template-rows: 1fr;
gap: 1rem;
}
.menu-btn {
padding: 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;
/* Kiosk Content */
.kiosk-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
cursor: pointer;
transition: all 0.15s ease;
width: 100%;
}
.menu-btn:hover {
background: rgba(30, 45, 70, 0.8);
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);
}
.menu-icon {
font-size: 2rem;
}
.menu-text {
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(200, 220, 240, 0.95);
}
/* Quick Amounts */
.withdraw-display,
.deposit-display,
.transfer-display {
flex: 1;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.quick-amounts {
/* Kiosk Grid */
.kiosk-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
gap: 1.5rem;
margin-top: 2rem;
width: 100%;
max-width: 600px;
}
.amount-btn {
padding: 1rem;
background: rgba(20, 30, 45, 0.7);
border: 1px solid rgba(100, 150, 200, 0.4);
border-radius: 4px;
color: rgba(100, 200, 150, 1);
font-size: 1.125rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
}
.amount-btn:hover {
background: rgba(30, 45, 70, 0.9);
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);
}
/* Custom Amount */
.custom-amount {
/* Kiosk Menu Stack */
.kiosk-menu-stack {
display: flex;
flex-direction: column;
gap: 0.75rem;
gap: 1.5rem;
margin-top: 2rem;
width: 100%;
max-width: 600px;
}
.custom-amount label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(140, 160, 180, 0.85);
}
/* Form Fields */
.transfer-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-field label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(140, 160, 180, 0.85);
}
.amount-input,
.text-input {
padding: 0.875rem 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: 1rem;
transition: all 0.15s ease;
}
.amount-input:focus,
.text-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);
}
.amount-input::placeholder,
.text-input::placeholder {
color: rgba(100, 120, 140, 0.6);
}
/* Balance Display */
.balance-display {
display: flex;
flex-direction: column;
gap: 1rem;
flex: 1;
}
.balance-item {
padding: 1.25rem;
background: rgba(20, 30, 45, 0.6);
border: 1px solid rgba(100, 150, 200, 0.3);
border-left: 3px solid rgba(100, 150, 200, 0.5);
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.balance-label {
font-size: 0.875rem;
color: rgba(160, 180, 200, 0.85);
}
.balance-amount {
/* Kiosk Button */
.kiosk-btn {
padding: 2rem;
font-size: 1.25rem;
font-weight: 600;
color: rgba(100, 200, 150, 1);
}
.balance-total {
border-left-color: rgba(100, 200, 150, 0.6);
background: rgba(30, 45, 70, 0.7);
}
.balance-total .balance-label {
font-size: 1rem;
font-weight: 600;
color: rgba(200, 220, 255, 1);
}
.balance-total .balance-amount {
font-size: 1.5rem;
}
/* Deposit Info */
.atm-btn-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.deposit-info {
padding: 1rem;
background: rgba(20, 30, 45, 0.5);
border: 1px solid rgba(100, 150, 200, 0.2);
border-radius: 4px;
text-align: center;
}
.deposit-info p {
font-size: 0.875rem;
color: rgba(160, 180, 200, 0.85);
}
.deposit-info span {
font-weight: 600;
color: rgba(100, 200, 150, 1);
}
/* Transaction Result */
.transaction-result {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 2rem 1rem;
flex: 1;
}
.result-icon {
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
font-weight: bold;
}
.transaction-result.success .result-icon {
background: rgba(100, 200, 150, 0.2);
border: 3px solid rgba(100, 200, 150, 0.6);
color: rgba(150, 255, 200, 1);
box-shadow: 0 0 20px rgba(100, 200, 150, 0.3);
}
.transaction-result.error .result-icon {
background: rgba(200, 100, 100, 0.2);
border: 3px solid rgba(200, 100, 100, 0.6);
color: rgba(255, 150, 150, 1);
box-shadow: 0 0 20px rgba(200, 100, 100, 0.3);
}
.transaction-result h3 {
gap: 0.5rem;
height: 100%;
min-height: 120px;
margin: 0;
padding: 0;
border: none;
}
.transaction-result p {
font-size: 0.875rem;
color: rgba(160, 180, 200, 0.85);
text-align: center;
}
/* Buttons */
.atm-btn {
padding: 1rem;
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;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
button {
background: var(--primary);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: var(--radius);
cursor: pointer;
transition: all 0.15s ease;
}
font-size: 1rem;
font-weight: 500;
font-family: inherit;
transition: all 0.2s ease;
.atm-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);
}
.atm-btn-primary {
background: rgba(100, 150, 200, 0.2);
border-color: rgba(100, 150, 200, 0.5);
}
.atm-btn-primary:hover {
background: rgba(100, 150, 200, 0.3);
border-color: rgba(150, 200, 255, 0.7);
}
.atm-btn-secondary {
background: rgba(200, 150, 100, 0.2);
border-color: rgba(200, 150, 100, 0.4);
}
.atm-btn-secondary:hover {
background: rgba(200, 150, 100, 0.3);
border-color: rgba(255, 200, 150, 0.6);
}
.atm-btn-full {
background: rgba(100, 200, 150, 0.2);
border-color: rgba(100, 200, 150, 0.4);
}
.atm-btn-full:hover {
background: rgba(100, 200, 150, 0.3);
border-color: rgba(150, 255, 200, 0.6);
}
/* Footer */
.atm-footer {
padding: 1rem 1.5rem;
background: rgba(20, 30, 45, 0.9);
border-top: 2px solid rgba(100, 150, 200, 0.3);
text-align: center;
}
.footer-text {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 1px;
color: rgba(100, 150, 200, 0.7);
}
/* Responsive */
@media (max-width: 768px) {
.atm-container {
justify-content: center;
padding: 1rem;
&:hover {
background: var(--primary-hover);
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
.atm-screen {
transform: none;
width: 100%;
max-width: 450px;
&+& {
margin-left: 1rem;
}
}

View File

@ -5,8 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ATM</title>
<!-- <script src="store.js"></script> -->
<!-- <link rel="stylesheet" href="atm.css" /> -->
<!-- <link rel="stylesheet" href="atm.css"> -->
<!--
Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API
@ -14,15 +13,9 @@
-->
<script>
Promise.all([
A3API.RequestFile(
"forge\\forge_client\\addons\\bank\\ui\\_site\\atm.css",
),
A3API.RequestFile(
"forge\\forge_client\\addons\\bank\\ui\\_site\\store.js",
),
A3API.RequestFile(
"forge\\forge_client\\addons\\bank\\ui\\_site\\atm.js",
),
A3API.RequestFile("forge\\forge_client\\addons\\bank\\ui\\_site\\atm.css"),
A3API.RequestFile("forge\\forge_client\\addons\\bank\\ui\\_site\\store.js"),
A3API.RequestFile("forge\\forge_client\\addons\\bank\\ui\\_site\\atm.js"),
]).then(([css, storeJs, atmJs]) => {
const style = document.createElement("style");
style.textContent = css;
@ -40,149 +33,8 @@
</head>
<body>
<div class="atm-container">
<div class="atm-screen">
<!-- Header -->
<div class="atm-header">
<div class="atm-logo">💳</div>
<div class="atm-title">AUTOMATED TELLER</div>
</div>
<!-- Main Content Area -->
<div class="atm-content" id="atmContent">
<!-- Welcome Screen -->
<div class="atm-view" id="welcomeView">
<div class="welcome-message">
<div class="welcome-icon">👤</div>
<h2>Welcome</h2>
<p>Insert your card to begin</p>
</div>
<button class="atm-btn atm-btn-primary" onclick="showView('pinView')">
Insert Card
</button>
</div>
<!-- PIN Entry Screen -->
<div class="atm-view" id="pinView" style="display: none;">
<div class="pin-entry">
<h3>Enter PIN</h3>
<div class="pin-display">
<span class="pin-dot"></span>
<span class="pin-dot"></span>
<span class="pin-dot"></span>
<span class="pin-dot"></span>
</div>
<div class="keypad" id="keypad">
<!-- Keypad buttons will be generated by JavaScript -->
</div>
</div>
</div>
<!-- Main Menu Screen -->
<div class="atm-view" id="menuView" style="display: none;">
<div class="account-summary">
<div class="summary-item">
<span class="summary-label">Cash</span>
<span class="summary-value" id="cashBalance">$2,500</span>
</div>
<div class="summary-item">
<span class="summary-label">Bank</span>
<span class="summary-value" id="bankBalance">$45,750</span>
</div>
</div>
<div class="menu-options">
<button class="menu-btn" onclick="showView('withdrawView')">
<span class="menu-text">Withdraw</span>
</button>
<button class="menu-btn" onclick="showView('balanceView')">
<span class="menu-text">Balance</span>
</button>
</div>
<button class="atm-btn atm-btn-secondary" onclick="exitATM()">
Exit
</button>
</div>
<!-- Withdraw Screen -->
<div class="atm-view" id="withdrawView" style="display: none;">
<h3>Withdraw Cash</h3>
<div class="withdraw-display">
<div class="quick-amounts">
<button class="amount-btn" onclick="withdrawAmount(100)">$100</button>
<button class="amount-btn" onclick="withdrawAmount(500)">$500</button>
<button class="amount-btn" onclick="withdrawAmount(1000)">$1,000</button>
<button class="amount-btn" onclick="withdrawAmount(2000)">$2,000</button>
</div>
<div class="custom-amount">
<label>Custom Amount</label>
<input type="number" class="amount-input" id="withdrawInput" placeholder="0.00" min="0"
step="1">
</div>
</div>
<div class="atm-btn-group">
<button class="atm-btn atm-btn-primary" onclick="withdrawCustom()">
Withdraw
</button>
<button class="atm-btn atm-btn-secondary" onclick="showView('menuView')">
Back
</button>
</div>
</div>
<!-- Balance Screen -->
<div class="atm-view" id="balanceView" style="display: none;">
<h3>Account Balance</h3>
<div class="balance-display">
<div class="balance-item">
<span class="balance-label">Cash on Hand</span>
<span class="balance-amount" id="cashBalanceDetail">$2,500</span>
</div>
<div class="balance-item">
<span class="balance-label">Bank Account</span>
<span class="balance-amount" id="bankBalanceDetail">$45,750</span>
</div>
<div class="balance-item balance-total">
<span class="balance-label">Total Assets</span>
<span class="balance-amount" id="totalBalance">$48,250</span>
</div>
</div>
<button class="atm-btn atm-btn-secondary" onclick="showView('menuView')">
Back
</button>
</div>
<!-- Transaction Success Screen -->
<div class="atm-view" id="successView" style="display: none;">
<div class="transaction-result success">
<div class="result-icon"></div>
<h3>Transaction Complete</h3>
<p id="successMessage">Your transaction was successful</p>
</div>
<button class="atm-btn atm-btn-primary" onclick="showView('menuView')">
Continue
</button>
</div>
<!-- Transaction Error Screen -->
<div class="atm-view" id="errorView" style="display: none;">
<div class="transaction-result error">
<div class="result-icon"></div>
<h3>Transaction Failed</h3>
<p id="errorMessage">An error occurred</p>
</div>
<button class="atm-btn atm-btn-secondary" onclick="goBackFromError()">
Back
</button>
</div>
</div>
<!-- Footer -->
<div class="atm-footer">
<div class="footer-text">Secure Banking • 24/7 Access</div>
</div>
</div>
</div>
<div id="app"></div>
<!-- <script src="store.js"></script> -->
<!-- <script src="atm.js"></script> -->
</body>

View File

@ -1,380 +1,332 @@
/**
* ATM Interface
* Handles banking transactions with PIN authentication
* ATM App - Vanilla JS Kiosk Implementation
*/
// ============================================================================
// STATE
// ============================================================================
//=============================================================================
// #region LIBRARY - DOM Helper
//=============================================================================
let enteredPin = '';
let currentView = 'welcomeView';
let previousView = 'welcomeView';
// ============================================================================
// VIEW MANAGEMENT
// ============================================================================
function showView(viewId) {
// Hide all views
document.querySelectorAll('.atm-view').forEach(view => {
view.style.display = 'none';
});
// Show selected view
const view = document.getElementById(viewId);
if (view) {
view.style.display = 'flex';
previousView = currentView;
currentView = viewId;
// Update balance displays when showing certain views
if (viewId === 'menuView' || viewId === 'balanceView' || viewId === 'depositView') {
updateBalances();
function h(tag, props = {}, ...children) {
const el = document.createElement(tag);
if (props) {
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith('on') && typeof value === 'function') {
el.addEventListener(key.substring(2).toLowerCase(), value);
} else if (key === 'className') {
el.className = value;
} else if (key === 'style' && typeof value === 'object') {
Object.assign(el.style, value);
} else {
el.setAttribute(key, value);
}
});
}
children.forEach(child => {
if (typeof child === 'string' || typeof child === 'number') {
el.appendChild(document.createTextNode(child));
} else if (child instanceof Node) {
el.appendChild(child);
} else if (Array.isArray(child)) {
child.forEach(c => {
if (c instanceof Node) el.appendChild(c);
});
}
});
return el;
}
let _rootContainer = null;
let _rootComponent = null;
function render(component, container) {
_rootContainer = container;
_rootComponent = component;
_render();
}
function _render() {
if (_rootContainer && _rootComponent) {
_rootContainer.innerHTML = '';
_rootContainer.appendChild(_rootComponent());
}
}
// ============================================================================
// PIN AUTHENTICATION
// ============================================================================
const createSignal = (initialValue) => {
let _val = initialValue;
const getValue = () => _val;
const setValue = (newValue) => {
_val = typeof newValue === 'function' ? newValue(_val) : newValue;
_render();
};
return [getValue, setValue];
};
function generateKeypad() {
const keypad = document.getElementById('keypad');
if (!keypad) return;
//=============================================================================
// #region STATE
//=============================================================================
// 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' }
];
const [getView, setView] = createSignal('pin'); // 'pin', 'menu', 'withdraw', 'custom_withdraw', 'balance'
const [getPin, setPin] = createSignal('');
const [getCustomAmount, setCustomAmount] = createSignal('');
const [getMessage, setMessage] = createSignal('');
// Clear existing keypad
keypad.innerHTML = '';
//=============================================================================
// #region UI COMPONENTS
//=============================================================================
// Generate buttons
keys.forEach(key => {
const button = document.createElement('button');
button.className = `key-btn${key.class ? ' ' + key.class : ''}`;
button.textContent = key.label;
function Header() {
return h('div', { className: 'header', style: { marginBottom: '2rem' } },
h('h1', null, 'ATM TERMINAL'),
h('p', null, 'Global Financial Network')
);
}
// 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();
function PinView() {
const currentPin = getPin();
const handleNumClick = (num) => {
if (currentPin.length < 4) {
setPin(prev => prev + num);
}
};
keypad.appendChild(button);
});
}
const handleClear = () => setPin('');
function enterPin(digit) {
if (enteredPin.length < 4) {
enteredPin += digit;
updatePinDisplay();
}
}
function clearPin() {
enteredPin = '';
updatePinDisplay();
}
function updatePinDisplay() {
const dots = document.querySelectorAll('.pin-dot');
dots.forEach((dot, index) => {
if (index < enteredPin.length) {
dot.classList.add('filled');
const handleEnter = () => {
if (currentPin.length === 4) {
const state = typeof store !== 'undefined' ? store.getState() : { pin: '1234' };
if (currentPin === state.pin) {
setView('menu');
} else {
setMessage('Incorrect PIN');
setPin('');
setTimeout(() => setMessage(''), 2000);
}
} else {
dot.classList.remove('filled');
setMessage('Invalid PIN Length');
setTimeout(() => setMessage(''), 2000);
}
});
};
return h('div', { className: 'card', style: { padding: '3rem 2rem' } },
h('h2', null, 'Enter Security PIN'),
h('div', { className: 'pin-display' },
currentPin.replace(/./g, String.fromCharCode(8226)) || '----'
),
h('p', { style: { color: '#ef4444', height: '1.5rem', textAlign: 'center' } }, getMessage()),
h('div', { className: 'numpad' },
['1', '2', '3', '4', '5', '6', '7', '8', '9'].map(num =>
h('button', { onClick: () => handleNumClick(num) }, num)
),
h('button', { style: { background: '#ef4444', color: 'white' }, onClick: handleClear }, 'C'),
h('button', { onClick: () => handleNumClick('0') }, '0'),
h('button', { style: { background: '#10b981', color: 'white' }, onClick: handleEnter }, String.fromCharCode(8629))
)
);
}
function submitPin() {
if (enteredPin.length !== 4) {
showError('Please enter a 4-digit PIN');
return;
}
// In a real implementation, this would validate with the server
const currentState = store.getState();
if (enteredPin === currentState.pin) {
enteredPin = '';
updatePinDisplay();
showView('menuView');
} else {
showError('Incorrect PIN');
clearPin();
}
function MenuView() {
return h('div', { className: 'kiosk-content' },
h('h2', { style: { textAlign: 'center', marginBottom: '1rem' } }, 'Select Transaction'),
h('div', { className: 'kiosk-menu-stack' },
h('button', { className: 'kiosk-btn', onClick: () => setView('withdraw') },
'Withdraw Cash'
),
h('button', { className: 'kiosk-btn', onClick: () => setView('balance') },
'Check Balance'
),
h('button', {
className: 'kiosk-btn',
style: { background: 'var(--bg-surface)', color: 'var(--text-main)', border: '1px solid var(--border)' },
onClick: () => {
setPin('');
setView('pin');
sendEvent('atm::close', {});
}
}, 'Cancel Transaction')
)
);
}
// ============================================================================
// BALANCE MANAGEMENT
// ============================================================================
function WithdrawView() {
const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
const bankBalance = state.accounts?.bank || 0;
function updateBalances() {
const currentState = store.getState();
const handleWithdraw = (amount) => {
if (bankBalance >= amount) {
if (typeof store !== 'undefined') {
store.dispatch(withdraw(amount));
}
sendEvent('atm::withdraw', { amount });
setMessage(`Please take your cash: $${amount.toLocaleString()}`);
setTimeout(() => {
setMessage('');
setView('menu');
}, 3000);
} else {
setMessage('Insufficient Funds');
setTimeout(() => setMessage(''), 2000);
}
};
// 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 = `$${currentState.accounts.cash.toLocaleString()}`;
});
bankElements.forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = `$${currentState.accounts.bank.toLocaleString()}`;
});
const totalEl = document.getElementById('totalBalance');
if (totalEl) {
const total = currentState.accounts.cash + currentState.accounts.bank;
totalEl.textContent = `$${total.toLocaleString()}`;
if (getMessage()) {
return h('div', { className: 'card', style: { padding: '4rem', textAlign: 'center' } },
h('h2', { style: { color: 'var(--primary)' } }, getMessage())
);
}
return h('div', { className: 'kiosk-content' },
h('h2', { style: { textAlign: 'center', marginBottom: '1rem' } }, 'Select Amount'),
h('div', { className: 'kiosk-grid' },
h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(20) }, '$20'),
h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(50) }, '$50'),
h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(100) }, '$100'),
h('button', {
className: 'kiosk-btn',
onClick: () => {
setCustomAmount('');
setView('custom_withdraw');
}
}, 'Other Amount'),
h('button', { className: 'kiosk-btn', style: { gridColumn: 'span 2', background: 'var(--text-muted)' }, onClick: () => setView('menu') }, 'Cancel')
)
);
}
// ============================================================================
// WITHDRAW OPERATIONS
// ============================================================================
function CustomWithdrawView() {
const currentAmount = getCustomAmount();
const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
const bankBalance = state.accounts?.bank || 0;
function withdrawAmount(amount) {
const currentState = store.getState();
const handleNumClick = (num) => {
if (currentAmount.length < 5) {
setCustomAmount(prev => prev + num);
}
};
if (amount > currentState.accounts.bank) {
showError('Insufficient funds');
return;
const handleClear = () => setCustomAmount('');
const handleEnter = () => {
const amount = parseInt(currentAmount, 10);
if (amount > 0) {
if (bankBalance >= amount) {
if (typeof store !== 'undefined') {
store.dispatch(withdraw(amount));
}
sendEvent('atm::withdraw', { amount });
setMessage(`Please take your cash: $${amount.toLocaleString()}`);
setTimeout(() => {
setMessage('');
setView('menu');
}, 3000);
} else {
setMessage('Insufficient Funds');
setTimeout(() => setMessage(''), 2000);
}
} else {
setMessage('Invalid Amount');
setTimeout(() => setMessage(''), 2000);
}
};
if (getMessage()) {
return h('div', { className: 'card', style: { padding: '4rem', textAlign: 'center' } },
h('h2', { style: { color: 'var(--primary)' } }, getMessage())
);
}
store.dispatch(withdraw(amount));
sendEvent('atm::withdraw', { amount: amount });
showSuccess(`Withdrew $${amount.toLocaleString()}`);
return h('div', { className: 'card', style: { padding: '3rem 2rem' } },
h('h2', null, 'Enter Amount'),
h('div', { className: 'pin-display' },
currentAmount ? `$${currentAmount}` : '$0'
),
h('div', { className: 'numpad' },
['1', '2', '3', '4', '5', '6', '7', '8', '9'].map(num =>
h('button', { onClick: () => handleNumClick(num) }, num)
),
h('button', { style: { background: '#ef4444', color: 'white' }, onClick: handleClear }, 'C'),
h('button', { onClick: () => handleNumClick('0') }, '0'),
h('button', { style: { background: '#10b981', color: 'white' }, onClick: handleEnter }, String.fromCharCode(8629))
),
h('button', {
style: { width: '100%', marginTop: '2rem', padding: '1rem', background: 'var(--text-muted)' },
onClick: () => setView('withdraw')
}, 'Cancel')
);
}
function withdrawCustom() {
const input = document.getElementById('withdrawInput');
const amount = parseFloat(input.value);
function BalanceView() {
const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
const bankBalance = state.accounts?.bank || 0;
if (!amount || amount <= 0) {
showError('Please enter a valid amount');
return;
}
const currentState = store.getState();
if (amount > currentState.accounts.bank) {
showError('Insufficient funds');
return;
}
store.dispatch(withdraw(amount));
sendEvent('atm::withdraw', { amount: amount });
input.value = '';
showSuccess(`Withdrew $${amount.toLocaleString()}`);
return h('div', { className: 'card', style: { textAlign: 'center', padding: '3rem' } },
h('h2', { style: { color: 'var(--text-muted)' } }, 'Available Balance'),
h('div', { style: { fontSize: '4rem', fontWeight: '800', margin: '2rem 0', color: 'var(--primary-hover)' } },
'$' + bankBalance.toLocaleString()
),
h('button', { className: 'kiosk-btn', style: { width: '100%', maxWidth: '300px', margin: '0 auto' }, onClick: () => setView('menu') }, 'Return to Menu')
);
}
// ============================================================================
// DEPOSIT OPERATIONS
// ============================================================================
function App() {
const view = getView();
/**
* Deposits specified amount into bank account
* @deprecated Use store actions instead
*/
function depositAmount() {
const input = document.getElementById('depositInput');
const amount = parseFloat(input.value);
if (!amount || amount <= 0) {
showError('Please enter a valid amount');
return;
let mainContent;
if (view === 'pin') {
mainContent = PinView();
} else if (view === 'menu') {
mainContent = MenuView();
} else if (view === 'withdraw') {
mainContent = WithdrawView();
} else if (view === 'custom_withdraw') {
mainContent = CustomWithdrawView();
} else if (view === 'balance') {
mainContent = BalanceView();
}
const currentState = store.getState();
if (amount > currentState.accounts.cash) {
showError('Insufficient cash');
return;
}
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() {
const currentState = store.getState();
if (currentState.accounts.cash <= 0) {
showError('No cash to deposit');
return;
}
const amount = currentState.accounts.cash;
store.dispatch(deposit(amount));
sendEvent('atm::deposit', { amount: amount });
showSuccess(`Deposited $${amount.toLocaleString()}`);
return h('main', null,
h('div', { className: 'container' },
Header(),
mainContent
)
);
}
// ============================================================================
// 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');
//=============================================================================
// #region ARMA 3 INTEGRATION
//=============================================================================
const playerId = playerIdInput.value.trim();
const amount = parseFloat(amountInput.value);
if (!playerId) {
showError('Please enter a player ID');
return;
}
if (!amount || amount <= 0) {
showError('Please enter a valid amount');
return;
}
const currentState = store.getState();
if (amount > currentState.accounts.bank) {
showError('Insufficient funds');
return;
}
store.dispatch(transfer('bank', amount, 'player'));
sendEvent('atm::transfer', {
playerId: playerId,
amount: amount
});
playerIdInput.value = '';
amountInput.value = '';
showSuccess(`Transferred $${amount.toLocaleString()} to Player ${playerId}`);
}
// ============================================================================
// RESULT SCREENS
// ============================================================================
function showSuccess(message) {
document.getElementById('successMessage').textContent = message;
showView('successView');
updateBalances();
}
function showError(message) {
document.getElementById('errorMessage').textContent = message;
showView('errorView');
}
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::close', {});
showView('welcomeView');
}
// ============================================================================
// 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
}));
A3API.SendAlert(JSON.stringify({ event, data }));
} else {
console.log('Event:', event, 'Data:', data);
}
}
// ============================================================================
// INITIALIZATION
// ============================================================================
//=============================================================================
// #region INITIALIZATION
//=============================================================================
let initialized = false;
function initATM() {
// Subscribe to store updates
if (typeof store !== 'undefined') {
store.subscribe(() => {
updateBalances();
});
if (initialized) return;
const root = document.getElementById('app');
if (root) {
if (typeof store !== 'undefined') {
store.subscribe(() => _render());
}
render(App, root);
initialized = true;
console.log('[ATM] Interface initialized');
}
// Generate keypad
generateKeypad();
// Show welcome screen
showView('welcomeView');
// Update initial balances
updateBalances();
console.log('[ATM] Interface initialized');
}
// Auto-initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initATM);
} else {
initATM();
}
// ============================================================================
// GLOBAL EXPORTS
// ============================================================================
window.showView = showView;
window.generateKeypad = generateKeypad;
window.enterPin = enterPin;
window.clearPin = clearPin;
window.submitPin = submitPin;
window.withdrawAmount = withdrawAmount;
window.withdrawCustom = withdrawCustom;
window.depositAmount = depositAmount;
window.depositAll = depositAll;
window.transferFunds = transferFunds;
window.goBackFromError = goBackFromError;
window.exitATM = exitATM;

View File

@ -1,449 +1,345 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
:root {
--bg-app: #fdfcf8;
--bg-surface: #ffffff;
--bg-surface-hover: #f1f5f9;
--primary: #475569;
--primary-hover: #1e293b;
--text-main: #1f2937;
--text-muted: #64748b;
--text-inverse: #f8fafc;
--border: #e2e8f0;
--radius: 8px;
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--footer-bg: #1e293b;
}
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;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
margin: 0;
padding: 0;
background: var(--bg-app);
color: var(--text-main);
line-height: 1.6;
}
.bank-container {
height: 100vh;
width: 100vw;
padding: 2rem;
#app {
min-height: 100vh;
}
main {
display: flex;
flex-direction: column;
gap: 1.5rem;
min-height: 100vh;
}
.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 {
.container {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 2rem;
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;
flex-direction: column;
box-sizing: border-box;
}
/* Navbar */
.navbar {
background: var(--bg-surface);
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow);
}
.navbar-inner {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 1rem 2rem;
box-sizing: border-box;
}
.navbar-brand {
display: flex;
align-items: center;
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);
}
}
.navbar-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--primary-hover);
letter-spacing: -0.025em;
}
.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;
.navbar-profile {
display: flex;
align-items: center;
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;
.profile-info {
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;
}
align-items: flex-end;
gap: 0.125rem;
}
.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;
.profile-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(200, 220, 255, 1);
letter-spacing: 0.05em;
color: var(--text-muted);
font-weight: 500;
}
.panel-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
.profile-id {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-main);
font-family: 'Consolas', 'Monaco', monospace;
}
&::-webkit-scrollbar {
width: 8px;
.btn-signout {
background: transparent;
color: var(--text-muted);
border: 1px solid var(--border);
padding: 0.5rem 1rem;
font-size: 0.85rem;
&-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);
}
}
&:hover {
background: var(--bg-surface-hover);
color: var(--primary-hover);
border-color: var(--primary);
transform: none;
box-shadow: none;
}
}
.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 {
.content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
&:last-child {
margin-bottom: 0;
/* Cards */
.card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem;
margin-bottom: 2rem;
box-shadow: var(--shadow);
text-align: center;
h2 {
margin-top: 0;
font-size: 1.8rem;
color: var(--primary-hover);
}
}
/* Buttons */
button {
background: var(--primary);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: var(--radius);
cursor: pointer;
font-size: 1rem;
font-weight: 500;
font-family: inherit;
transition: all 0.2s ease;
&:hover {
background: var(--primary-hover);
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
.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;
&+& {
margin-left: 1rem;
}
}
/* Forms */
form {
display: flex;
flex-direction: column;
gap: 1rem;
text-align: left;
label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-muted);
font-weight: 500;
font-size: 0.9rem;
}
.transfer-form {
input,
select {
width: 100%;
padding: 0.75rem;
border-radius: var(--radius);
border: 1px solid var(--border);
background: var(--bg-app);
color: var(--text-main);
font-family: inherit;
font-size: 1rem;
box-sizing: border-box;
transition: border-color 0.2s;
&:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
}
.form-actions {
margin-top: 1rem;
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);
}
}
}
align-items: center;
}
}
input[type=number] {
-moz-appearance: textfield;
appearance: textfield;
margin: 0;
/* Deposit/Withdraw Form */
.balance-info {
display: flex;
justify-content: space-around;
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--bg-surface-hover);
border-radius: var(--radius);
}
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
.balance-info-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.balance-info-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
font-weight: 500;
}
.balance-info-value {
font-size: 1.25rem;
font-weight: 700;
color: var(--primary-hover);
&.cash {
color: #fbbf24;
}
}
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
.deposit-withdraw-form {
display: flex;
flex-direction: column;
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);
}
input {
text-align: center;
font-size: 1.25rem;
padding: 1rem;
}
}
.transaction-list {
.deposit-withdraw-buttons {
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;
}
button {
flex: 1;
.transaction-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
.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);
&:hover {
background: var(--primary);
transform: none;
box-shadow: none;
}
&.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;
.deposit-earnings-button {
display: flex;
gap: 0.75rem;
width: 50%;
margin: 0 auto;
button {
flex: 1;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
&:hover {
background: var(--primary);
transform: none;
box-shadow: none;
}
}
}
}
@media (max-width: 1200px) {
.bank-content {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto;
/* Footer */
.footer {
margin-top: auto;
background: var(--footer-bg);
color: var(--text-inverse);
display: block;
.wrapper {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 3rem 2rem;
box-sizing: border-box;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
}
.panel-main {
grid-column: 1;
h3 {
color: var(--text-inverse);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 700;
margin-bottom: 1.5rem;
border-bottom: 1px solid #475569;
padding-bottom: 0.5rem;
margin-right: 1rem;
}
ul {
li {
color: #cbd5e1;
font-size: 0.95rem;
margin-bottom: 0.75rem;
cursor: pointer;
transition: color 0.2s;
&:hover {
color: white;
}
}
}
}

View File

@ -1,12 +1,11 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Banking Services</title>
<!-- <script src="store.js"></script> -->
<!-- <link rel="stylesheet" href="bank.css" /> -->
<title>FDIC - Global Financial Network</title>
<!-- <link rel="stylesheet" href="bank.css"> -->
<!--
Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API
@ -28,151 +27,20 @@
style.textContent = css;
document.head.appendChild(style);
const store = document.createElement("script");
store.text = storeJs;
document.head.appendChild(store);
const storeScript = document.createElement("script");
storeScript.text = storeJs;
document.head.appendChild(storeScript);
const bank = document.createElement("script");
bank.text = bankJs;
document.head.appendChild(bank);
const bankScript = document.createElement("script");
bankScript.text = bankJs;
document.head.appendChild(bankScript);
});
</script>
</head>
<body>
<div class="bank-container">
<!-- Header Section -->
<div class="bank-header">
<div class="bank-logo">
<!-- <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>
<p class="bank-subtitle">Secure Financial Management</p>
</div>
<div class="header-actions">
<button class="action-btn close-btn">Close</button>
</div>
</div>
<!-- Main Content -->
<div class="bank-content">
<!-- Left Panel - Accounts -->
<div class="bank-panel">
<div class="panel-header">
<h2 class="panel-title">Your Accounts</h2>
</div>
<div class="panel-content">
<!-- Cash Account -->
<div class="account-card">
<div class="account-header">
<div class="account-info">
<span class="account-name">Cash</span>
<span class="account-type">Physical Currency</span>
</div>
</div>
<div class="account-balance">
<span class="balance-label">Available</span>
<span class="balance-amount">$2,500</span>
</div>
</div>
<!-- Bank Account -->
<div class="account-card">
<div class="account-header">
<div class="account-info">
<span class="account-name">Bank Account</span>
<span class="account-type">Savings • Protected</span>
</div>
</div>
<div class="account-balance">
<span class="balance-label">Available</span>
<span class="balance-amount">$45,750</span>
</div>
</div>
<!-- Organization Account -->
<div class="account-card">
<div class="account-header">
<div class="account-info">
<span class="account-name">Organization</span>
<span class="account-type">Shared • View Only</span>
</div>
</div>
<div class="account-balance">
<span class="balance-label">Available</span>
<span class="balance-amount">$125,000</span>
</div>
</div>
</div>
</div>
<!-- Center Panel - Actions -->
<div class="bank-panel panel-main">
<div class="panel-header">
<h2 class="panel-title">Quick Actions</h2>
</div>
<div class="panel-content">
<!-- Transfer Form -->
<div class="action-section">
<h3 class="section-title">Transfer Funds</h3>
<div class="transfer-form">
<div class="form-group">
<label class="form-label">From</label>
<select class="form-select" id="transferFrom">
<option value="bank" selected>Bank Account</option>
<option value="cash">Cash</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Amount</label>
<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">Select Player</label>
<select class="form-select" id="playerId">
<option value="" disabled selected>Select a player...</option>
</select>
</div>
</div>
</div>
<!-- Quick Actions -->
<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-label">Deposit All Cash</span>
</button>
<button class="quick-action-btn" data-action="withdraw">
<span class="quick-action-label">Withdraw</span>
</button>
<button class="quick-action-btn" id="transferBtn">
<span class="quick-action-label">Transfer Funds</span>
</button>
</div>
</div>
</div>
</div>
<!-- Right Panel - Recent Transactions -->
<div class="bank-panel">
<div class="panel-header">
<h2 class="panel-title">Recent Transactions</h2>
</div>
<div class="panel-content">
<div class="transaction-list">
</div>
</div>
</div>
</div>
</div>
<div id="app"></div>
<!-- <script src="store.js"></script> -->
<!-- <script src="bank.js"></script> -->
</body>

View File

@ -1,278 +1,341 @@
/**
* Banking Interface
* Handles transfers, deposits, withdrawals, and account management
* Bank App - Vanilla JS Implementation matching WIP UI
*/
// ============================================================================
// INITIALIZATION
// ============================================================================
//=============================================================================
// #region LIBRARY - DOM Helper
//=============================================================================
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;
function h(tag, props = {}, ...children) {
const el = document.createElement(tag);
if (props) {
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith('on') && typeof value === 'function') {
el.addEventListener(key.substring(2).toLowerCase(), value);
} else if (key === 'className') {
el.className = value;
} else if (key === 'style' && typeof value === 'object') {
Object.assign(el.style, value);
} else if (key === 'disabled' || key === 'checked' || key === 'selected' || key === 'readonly') {
if (value) el[key] = true;
} else {
el.setAttribute(key, value);
}
});
});
}
// ============================================================================
// 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);
}
children.forEach(child => {
if (typeof child === 'string' || typeof child === 'number') {
el.appendChild(document.createTextNode(child));
} else if (child instanceof Node) {
el.appendChild(child);
} else if (Array.isArray(child)) {
child.forEach(c => {
if (c instanceof Node) el.appendChild(c);
});
}
});
return el;
}
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;
}
}
let _rootContainer = null;
let _rootComponent = null;
function render(component, container) {
_rootContainer = container;
_rootComponent = component;
_render();
}
function _render() {
if (_rootContainer && _rootComponent) {
_rootContainer.innerHTML = '';
_rootContainer.appendChild(_rootComponent());
}
}
function renderTransactions() {
const transactionList = document.querySelector('.transaction-list');
if (!transactionList) return;
//=============================================================================
// #region UI COMPONENTS
//=============================================================================
transactionList.innerHTML = '';
function Navbar() {
const state = store.getState();
const uid = state.uid || 'Unknown';
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);
});
return h('nav', { className: 'navbar' },
h('div', { className: 'navbar-inner' },
h('div', { className: 'navbar-brand' },
h('span', { className: 'navbar-title' }, 'FDIC - Global Financial Network')
),
h('div', { className: 'navbar-profile' },
h('div', { className: 'profile-info' },
h('span', { className: 'profile-label' }, 'Account'),
h('span', { className: 'profile-id' }, uid)
),
h('button', {
className: 'btn-signout',
onClick: () => sendEvent('bank::close', {})
}, 'Sign Out')
)
)
);
}
// ============================================================================
// ARMA 3 INTEGRATION
// ============================================================================
function TransactionHistory() {
const state = store.getState();
const transactions = state.transactions || [];
return h('div', { className: 'card' },
h('h3', { style: { textAlign: 'left', borderBottom: '1px solid var(--border)', paddingBottom: '1rem', marginBottom: '1rem' } }, 'Recent Transactions'),
transactions.length === 0
? h('p', { style: { color: 'var(--text-muted)' } }, 'No transactions yet')
: h('ul', { style: { listStyle: 'none', padding: 0, margin: 0 } },
transactions.slice(0, 10).map(tx => {
const isCredit = tx.type === 'Deposit';
return h('li', {
style: {
display: 'flex',
justifyContent: 'space-between',
padding: '0.75rem 0',
borderBottom: '1px solid var(--bg-surface-hover)'
}
},
h('div', { style: { textAlign: 'left' } },
h('div', { style: { fontWeight: '500' } }, tx.type),
h('div', { style: { fontSize: '0.85rem', color: 'var(--text-muted)' } }, tx.date)
),
h('div', {
style: {
fontWeight: '700',
color: isCredit ? '#10b981' : '#ef4444'
}
}, (isCredit ? '+' : '-') + '$' + Math.abs(tx.amount).toLocaleString())
);
})
)
);
}
function DepositWithdrawForm() {
const state = store.getState();
const bankBalance = state.accounts.bank;
const cashBalance = state.accounts.cash;
const getAmount = () => {
const input = document.getElementById('deposit-withdraw-amount');
return parseFloat(input?.value) || 0;
};
const clearInput = () => {
const input = document.getElementById('deposit-withdraw-amount');
if (input) input.value = '';
};
const handleDeposit = () => {
const amount = getAmount();
if (!amount || amount <= 0) {
console.log('Please enter a valid amount');
return;
}
if (amount > cashBalance) {
console.log('Insufficient cash');
return;
}
sendEvent('bank::deposit', { amount });
store.dispatch(deposit(amount));
clearInput();
};
const handleWithdraw = () => {
const amount = getAmount();
if (!amount || amount <= 0) {
console.log('Please enter a valid amount');
return;
}
if (amount > bankBalance) {
console.log('Insufficient funds');
return;
}
sendEvent('bank::withdraw', { amount });
store.dispatch(withdraw(amount));
clearInput();
};
return h('div', { className: 'card' },
h('h2', null, 'Deposit / Withdraw'),
h('div', { className: 'balance-info' },
h('div', { className: 'balance-info-item' },
h('span', { className: 'balance-info-label' }, 'Cash'),
h('span', { className: 'balance-info-value cash' }, '$' + cashBalance.toLocaleString())
),
h('div', { className: 'balance-info-item' },
h('span', { className: 'balance-info-label' }, 'Bank'),
h('span', { className: 'balance-info-value' }, '$' + bankBalance.toLocaleString())
)
),
h('div', { className: 'deposit-withdraw-form' },
h('input', { id: 'deposit-withdraw-amount', type: 'number', placeholder: 'Enter amount...', min: '1' }),
h('div', { className: 'deposit-withdraw-buttons' },
h('button', { onClick: handleDeposit, disabled: cashBalance <= 0 }, 'Deposit'),
h('button', { onClick: handleWithdraw, disabled: bankBalance <= 0 }, 'Withdraw')
)
)
);
}
function TransferForm() {
const state = store.getState();
const players = state.accounts.players || {};
const currentUid = state.uid;
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const amount = parseFloat(formData.get('amount'));
const playerId = formData.get('playerId');
if (!amount || amount <= 0) {
console.log('Please enter a valid amount');
return;
}
const currentState = store.getState();
if (!playerId) {
console.log('Please select a recipient');
return;
}
if (amount > currentState.accounts.bank) {
console.log('Insufficient funds');
return;
}
sendEvent('bank::transfer', { from: 'bank', amount, target: playerId });
store.dispatch(transfer('bank', amount, 'player'));
e.target.reset();
};
// Build player options
const playerOptions = [h('option', { value: '', disabled: true, selected: true }, 'Select player...')];
Object.keys(players).forEach(uid => {
if (uid !== currentUid && players[uid]?.name) {
playerOptions.push(h('option', { value: uid }, players[uid].name));
}
});
return h('div', { className: 'card' },
h('h2', null, 'Wire Transfer'),
h('form', { onSubmit: handleSubmit },
h('div', null,
h('label', null, 'Recipient'),
h('select', { name: 'playerId' }, playerOptions)
),
h('div', null,
h('label', null, 'Amount'),
h('input', { name: 'amount', type: 'number', placeholder: '0.00' })
),
h('button', { type: 'submit' }, 'Send Funds')
)
);
}
function BankDashboard() {
const state = store.getState();
const bankBalance = state.accounts.bank;
const earnings = state.accounts.earnings;
return h('div', { className: 'content' },
h('div', { className: 'card', style: { gridColumn: 'span 2' } },
h('h2', { style: { fontSize: '1.2rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' } }, 'Account Balance'),
h('div', { style: { fontSize: '2.8rem', fontWeight: '800', color: 'var(--primary-hover)', margin: '1rem 0' } },
'$' + bankBalance.toLocaleString()
),
h('div', { style: { textAlign: 'center', color: 'var(--text-muted)', fontSize: '1.1rem', marginBottom: '1rem' } },
'Pending: ',
h('span', { style: { color: '#fbbf24', fontWeight: 'bold' } }, '$' + earnings.toLocaleString())
),
h('div', { className: 'deposit-earnings-button' },
h('button', {
onClick: () => {
sendEvent('bank::depositEarnings', { amount: earnings });
store.dispatch(depositEarnings(earnings));
}, disabled: earnings <= 0, style: { width: '25%' }
}, 'Deposit Earnings')
)
),
DepositWithdrawForm(),
TransferForm(),
h('div', { style: { gridColumn: 'span 2' } }, TransactionHistory())
);
}
function Footer() {
return h('div', { className: 'footer' },
h('div', { className: 'wrapper' },
h('div', null,
h('h3', null, 'Secure Banking'),
h('ul', { style: { listStyleType: 'none', padding: 0 } },
h('li', null, 'FDIC Insured'),
h('li', null, 'Fraud Protection'),
h('li', null, '24/7 Support'),
h('li', null, 'API Access')
)
),
h('div', null,
h('h3', null, 'Notices'),
h('ul', { style: { listStyleType: 'none', padding: 0 } },
h('li', null, 'Terms of Service'),
h('li', null, 'Privacy Policy'),
h('li', null, 'Interest Rates'),
h('li', null, 'Report Fraud')
)
)
)
);
}
function App() {
return h('main', null,
Navbar(),
h('div', { className: 'container' },
BankDashboard()
),
Footer()
);
}
//=============================================================================
// #region 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
}));
A3API.SendAlert(JSON.stringify({ event, data }));
} else {
console.log('Event:', event, 'Data:', data);
}
}
// ============================================================================
// AUTO-INITIALIZE
// ============================================================================
//=============================================================================
// #region INITIALIZATION
//=============================================================================
let initialized = false;
function initBank() {
if (initialized) return;
const root = document.getElementById('app');
if (root) {
if (typeof store !== 'undefined') {
store.subscribe(() => _render());
}
render(App, root);
initialized = true;
console.log('[Bank] Interface initialized');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initBank);

View File

@ -45,6 +45,7 @@ const initialState = {
accounts: {
bank: 0,
cash: 0,
earnings: 0,
org: 0
},
pin: '1234',
@ -56,6 +57,7 @@ const initialState = {
// ============================================================================
const DEPOSIT = 'DEPOSIT';
const DEPOSIT_EARNINGS = 'DEPOSIT_EARNINGS';
const WITHDRAW = 'WITHDRAW';
const TRANSFER = 'TRANSFER';
const UPDATE_ACCOUNTS = 'UPDATE_ACCOUNTS';
@ -70,6 +72,11 @@ const deposit = (amount) => ({
payload: amount
});
const depositEarnings = (amount) => ({
type: DEPOSIT_EARNINGS,
payload: amount
});
const withdraw = (amount) => ({
type: WITHDRAW,
payload: amount
@ -120,6 +127,28 @@ function appReducer(state = initialState, action) {
]
};
case DEPOSIT_EARNINGS:
if (state.accounts.earnings < action.payload) {
console.warn('Insufficient earnings!');
return state;
}
return {
...state,
accounts: {
...state.accounts,
bank: state.accounts.bank + action.payload,
earnings: state.accounts.earnings - action.payload
},
transactions: [
...state.transactions,
{
type: 'Deposit Earnings',
amount: action.payload,
date: new Date().toLocaleString()
}
]
};
case WITHDRAW:
if (state.accounts.bank < action.payload) {
console.warn('Insufficient funds!');
@ -227,8 +256,9 @@ 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.cash !== undefined) accounts.cash = data.cash;
if (data.earnings !== undefined) accounts.earnings = data.earnings;
if (data.org !== undefined) accounts.org = data.org;
if (data.players !== undefined) accounts.players = data.players;

View File

@ -5,6 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Forge - Notification System</title>
<!-- <link rel="stylesheet" href="styles.css"> -->
<!--
Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API
@ -12,17 +13,13 @@
-->
<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);
@ -31,8 +28,8 @@
</head>
<body>
<!-- Main notification container -->
<div id="notification-container" class="notification-container" role="region" aria-label="Notifications"></div>
<!-- <script src="script.js"></script> -->
</body>
</html>

View File

@ -1,3 +1,28 @@
:root {
--bg-surface: #ffffff;
--bg-surface-hover: #f1f5f9;
--primary: #475569;
--primary-hover: #1e293b;
--text-main: #1f2937;
--text-muted: #64748b;
--border: #e2e8f0;
--radius: 8px;
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--success: #10b981;
--success-bg: #ecfdf5;
--success-border: #a7f3d0;
--danger: #ef4444;
--danger-bg: #fef2f2;
--danger-border: #fecaca;
--warning: #f59e0b;
--warning-bg: #fffbeb;
--warning-border: #fde68a;
--info: #3b82f6;
--info-bg: #eff6ff;
--info-border: #bfdbfe;
}
* {
margin: 0;
padding: 0;
@ -5,37 +30,33 @@
}
body {
font-family: Arial, sans-serif;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: transparent;
min-height: 100vh;
color: rgba(200, 220, 240, 0.95);
color: var(--text-main);
pointer-events: none;
}
/* Notification Container */
.notification-container {
position: fixed;
top: 120px;
right: 20px;
top: 80px;
right: 24px;
z-index: 1000;
width: 350px;
width: 360px;
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;
background: var(--bg-surface);
border: 1px solid var(--border);
border-left: 4px solid var(--primary);
border-radius: var(--radius);
box-shadow: 0 4px 12px rgb(0 0 0 / 0.1), 0 2px 4px rgb(0 0 0 / 0.05);
margin-bottom: 12px;
padding: 1rem 1.25rem;
width: 100%;
transform: translateX(100%);
transition: all 0.15s ease;
transform: translateX(calc(100% + 24px));
transition: all 0.2s ease;
position: relative;
overflow: hidden;
@ -44,113 +65,128 @@ body {
}
&.hide {
transform: translateX(100%);
transform: translateX(calc(100% + 24px));
opacity: 0;
}
/* Notification Types */
&.success {
border-left-color: rgba(100, 200, 150, 0.6);
background: var(--success-bg);
border-color: var(--success-border);
border-left-color: var(--success);
.notification-title {
color: rgba(150, 255, 200, 0.9);
color: #065f46;
}
.notification-message {
color: #047857;
}
.notification-progress-bar {
background: rgba(100, 200, 150, 0.8);
animation: progress 5s linear forwards;
background: var(--success);
}
}
&.danger {
border-left-color: rgba(220, 100, 100, 0.6);
background: var(--danger-bg);
border-color: var(--danger-border);
border-left-color: var(--danger);
.notification-title {
color: rgba(255, 150, 150, 0.9);
color: #991b1b;
}
.notification-message {
color: #b91c1c;
}
.notification-progress-bar {
background: rgba(220, 100, 100, 0.8);
animation: progress 5s linear forwards;
background: var(--danger);
}
}
&.warning {
border-left-color: rgba(200, 150, 100, 0.6);
background: var(--warning-bg);
border-color: var(--warning-border);
border-left-color: var(--warning);
.notification-title {
color: rgba(255, 200, 150, 0.9);
color: #92400e;
}
.notification-message {
color: #b45309;
}
.notification-progress-bar {
background: rgba(200, 150, 100, 0.8);
animation: progress 5s linear forwards;
background: var(--warning);
}
}
&.info {
border-left-color: rgba(100, 150, 200, 0.6);
background: var(--info-bg);
border-color: var(--info-border);
border-left-color: var(--info);
.notification-title {
color: rgba(150, 200, 255, 0.9);
color: #1e40af;
}
.notification-message {
color: #1d4ed8;
}
.notification-progress-bar {
background: rgba(100, 150, 200, 0.8);
animation: progress 5s linear forwards;
background: var(--info);
}
}
}
/* Notification Content */
.notification-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
margin-bottom: 0.375rem;
}
.notification-title {
font-weight: 600;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 0.025em;
flex: 1;
color: rgba(200, 220, 255, 1);
color: var(--primary-hover);
}
.notification-message {
color: rgba(140, 160, 180, 0.9);
font-size: 0.8rem;
line-height: 1.4;
color: var(--text-muted);
font-size: 0.875rem;
line-height: 1.5;
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);
background: var(--bg-surface-hover);
width: 100%;
border-radius: 0 0 4px 4px;
border-radius: 0 0 var(--radius) var(--radius);
}
.notification-progress-bar {
height: 100%;
width: 0%;
transform-origin: left;
border-radius: 0 0 4px 4px;
border-radius: 0 0 var(--radius) var(--radius);
transition: width linear;
}
/* Responsive Design */
@media (max-width: 768px) {
.notification-container {
left: 20px;
right: 20px;
left: 24px;
right: 24px;
width: auto;
}
@ -169,6 +205,6 @@ body {
@media (max-width: 500px) {
.notification-container {
width: calc(100vw - 40px);
width: calc(100vw - 48px);
}
}

View File

@ -1 +1,2 @@
PREP(initBank);
PREP(initBankStore);

View File

@ -1 +1,3 @@
#include "script_component.hpp"
call FUNC(initBank);

View File

@ -102,3 +102,10 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "" || _amount isEqualTo 0) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Amount!" };
GVAR(BankStore) call ["withdraw", [_uid, _amount]];
}] call CFUNC(addEventHandler);
[QGVAR(requestDepositEarnings), {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
if (_uid isEqualTo "" || _amount isEqualTo 0) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Amount!" };
GVAR(BankStore) call ["depositEarnings", [_uid, _amount]];
}] call CFUNC(addEventHandler);

View File

@ -0,0 +1,49 @@
#include "..\script_component.hpp"
/*
* File: fnc_initBank.sqf
* Author: IDSolutions
* Date: 2025-12-17
* Last Update: 2026-02-17
* Public: No
*
* Description:
* Initializes all editor-placed banks.
*
* Arguments:
* None
*
* Return Value:
* None
*
* Example:
* call forge_server_bank_fnc_initBank
*/
private _atms = (allVariables missionNamespace) select {
private _var = missionNamespace getVariable _x;
("atm" in _x) && { _var isEqualType objNull } && { !isNull _var }
};
private _banks = (allVariables missionNamespace) select {
private _var = missionNamespace getVariable _x;
("bank" in _x) && { _var isEqualType objNull } && { !isNull _var }
};
if (_atms isNotEqualTo []) then {
{
private _atm = missionNamespace getVariable _x;
SETPVAR(_atm,isAtm,true);
} forEach _atms;
} else {
["INFO", "No editor-placed atms found."] call EFUNC(common,log);
};
if (_banks isNotEqualTo []) then {
{
private _bank = missionNamespace getVariable _x;
SETPVAR(_bank,isBank,true);
} forEach _banks;
} else {
["INFO", "No editor-placed banks found."] call EFUNC(common,log);
};

View File

@ -4,7 +4,7 @@
* File: fnc_initBankStore.sqf
* Author: IDSolutions
* Date: 2025-12-17
* Last Update: 2026-02-13
* Last Update: 2026-02-17
* Public: Yes
*
* Description:
@ -157,6 +157,8 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)], ["cash", (_cash - _amount)]];
private _player = [_uid] call EFUNC(common,getPlayer);
GVAR(Registry) set [_uid, _finalAccount];
[CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent);
[CRPC(notifications,recieveNotification), ["info", "Bank", format ["Deposited $%1", _amount]], _player] call CFUNC(targetEvent);
}],
@ -172,15 +174,15 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)]];
private _player = [_uid] call EFUNC(common,getPlayer);
GVAR(Registry) set [_uid, _finalAccount];
[CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent);
[CRPC(notifications,recieveNotification), ["info", "Bank", format ["Paid $%1", _amount]], _player] call CFUNC(targetEvent);
}],
["transfer", compileFinal {
params [["_uid", "", [""]], ["_target", "", [""]], ["_from", "", [""]], ["_amount", 0, [0]]];
if (_uid isEqualTo _target) exitWith {
["WARNING", format ["Self-transfer attempt blocked for %1", _uid]] call EFUNC(common,log);
};
if (_uid isEqualTo _target) exitWith { ["WARNING", format ["Self-transfer attempt blocked for %1", _uid]] call EFUNC(common,log); };
private _account = GVAR(Registry) getOrDefault [_uid, nil];
if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); };
@ -188,13 +190,16 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
private _targetAccount = GVAR(Registry) getOrDefault [_target, nil];
if (isNil "_targetAccount") exitWith { ["ERROR", "Empty/Invalid Target Account!"] call EFUNC(common,log); };
private _bank = _account getOrDefault [_from, 0];
if (_bank < _amount) exitWith { ["WARNING", "Insufficient Funds!"] call EFUNC(common,log); };
private _selected = _account getOrDefault [_from, 0];
if (_selected < _amount) exitWith { ["WARNING", "Insufficient Funds!"] call EFUNC(common,log); };
private _targetBank = _targetAccount getOrDefault ["bank", 0];
private _finalAccount = createHashMapFromArray [[_from, (_bank - _amount)]];
private _finalAccount = createHashMapFromArray [[_from, (_selected - _amount)]];
private _finalTargetBank = createHashMapFromArray [["bank", (_targetBank + _amount)]];
GVAR(Registry) set [_uid, _finalAccount];
GVAR(Registry) set [_target, _finalTargetBank];
private _player = [_uid] call EFUNC(common,getPlayer);
private _targetPlayer = [_target] call EFUNC(common,getPlayer);
@ -218,8 +223,30 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
private _finalAccount = createHashMapFromArray [["bank", (_bank - _amount)], ["cash", (_cash + _amount)]];
private _player = [_uid] call EFUNC(common,getPlayer);
GVAR(Registry) set [_uid, _finalAccount];
[CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent);
[CRPC(notifications,recieveNotification), ["info", "Bank", format ["Withdrew $%1", _amount]], _player] call CFUNC(targetEvent);
}],
["depositEarnings", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
["INFO", format ["Deposit Earnings %1, for %2", _amount, _uid]] call EFUNC(common,log);
private _account = GVAR(Registry) getOrDefault [_uid, nil];
if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); };
private _bank = _account getOrDefault ["bank", 0];
private _earnings = _account getOrDefault ["earnings", 0];
if (_earnings < _amount) exitWith { ["WARNING", "Insufficient Earnings!"] call EFUNC(common,log); };
private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)], ["earnings", (_earnings - _amount)]];
private _player = [_uid] call EFUNC(common,getPlayer);
GVAR(Registry) set [_uid, _finalAccount];
[CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent);
[CRPC(notifications,recieveNotification), ["info", "Bank", format ["Deposited $%1 from earnings", _amount]], _player] call CFUNC(targetEvent);
}]
];