+
-
+
-
+
-
+
+
+
-
+
Global Actions
@@ -47,7 +76,7 @@
-
+
Give All Money
@@ -57,7 +86,7 @@
-
+
Broadcast Message
@@ -68,24 +97,37 @@
+
+
-
+
+
+
+
-
+
@@ -100,7 +142,7 @@
diff --git a/addons/admin/ui/_site/script.js b/addons/admin/ui/_site/script.js
index dd03a61..b4bc5bc 100644
--- a/addons/admin/ui/_site/script.js
+++ b/addons/admin/ui/_site/script.js
@@ -1,60 +1,152 @@
-// Simulated admin data - this would be replaced with actual game data
+/**
+ * Admin Panel Management Script
+ * This script handles the admin panel functionality for the Arma 3 game interface.
+ * It provides player management, money operations, messaging, and other admin functions.
+ */
+
+//=============================================================================
+// #region DATA STRUCTURES AND VARIABLES
+//=============================================================================
+
+/**
+ * Admin data structure - will be populated from the game
+ * Contains player information and payday amount configuration
+ */
let adminData = {
- players: [
- {
- id: 1,
- name: "John_Doe",
- rank: 5,
- money: 50000,
- status: "online"
- },
- {
- id: 2,
- name: "Jane_Smith",
- rank: 3,
- money: 25000,
- status: "online"
- },
- {
- id: 3,
- name: "Mike_Johnson",
- rank: 1,
- money: 10000,
- status: "offline"
- }
- ],
- paydayAmounts: {
- 1: 1000, // Rank 1 (Player) payday amount
- 2: 2000, // Rank 2 payday amount
- 3: 3000, // Rank 3 payday amount
- 4: 4000, // Rank 4 payday amount
- 5: 5000 // Rank 5 (Admin) payday amount
- },
- maxRank: 5
+ players: [], // List of all players with their details
+ paydayAmounts: {} // Map of paygrade to bonus amount
};
+/**
+ * Currently selected player ID for operations that require a player selection
+ * @type {string|null}
+ */
let selectedPlayerId = null;
-// Initialize the admin panel
+// #endregion
+
+//=============================================================================
+// #region INITIALIZATION AND DATA REQUESTS
+//=============================================================================
+
+/**
+ * Initialize the admin panel
+ * Sets up the UI, requests initial data from the game engine
+ */
function initializeAdmin() {
updateStats();
setupFilterListeners();
+ requestPlayerData();
+ requestPaygradeData();
+}
+
+/**
+ * Request player data from the game engine
+ * Sends an event to fetch current player information
+ */
+function requestPlayerData() {
+ const message = {
+ event: "REQUEST::PLAYER::DATA",
+ data: {}
+ };
+
+ // Send request to the game engine
+ A3API.SendAlert(JSON.stringify(message));
+}
+
+/**
+ * Request paygrade data from the game engine
+ * Sends an event to fetch current paygrade configuration
+ */
+function requestPaygradeData() {
+ const message = {
+ event: "REQUEST::PAYGRADE::DATA",
+ data: {}
+ };
+
+ // Send request to the game engine
+ A3API.SendAlert(JSON.stringify(message));
+}
+
+/**
+ * Set up a timer to periodically refresh player data
+ * Ensures the UI is updated with the latest information
+ */
+function setupRefreshTimer() {
+ setInterval(requestPlayerData, 30000); // Refresh every 30 seconds
+}
+
+// #endregion
+
+//=============================================================================
+// #region DATA HANDLERS
+//=============================================================================
+
+/**
+ * Handle paygrade data received from the game engine
+ * Processes the paygrade list and updates the UI accordingly
+ *
+ * @param {Array} paygradeList - List of paygrade objects with paygrade and bonus properties
+ */
+function handlePaygradeDataRequest(paygradeList) {
+ try {
+ // Convert the paygrade list to a map for easier lookup
+ const paygradeMap = {};
+ paygradeList.forEach(item => {
+ paygradeMap[item.paygrade] = item.bonus;
+ });
+
+ adminData.paydayAmounts = paygradeMap;
+
+ // Update the player list if we already have player data
+ if (adminData.players.length > 0) {
+ updatePlayerList();
+ }
+
+ console.log("Paygrade data updated successfully");
+ } catch (error) {
+ console.error("Error updating paygrade data:", error);
+ }
+}
+
+/**
+ * Handle player data received from the game engine
+ * Updates the admin panel with current player information
+ *
+ * @param {Array} playerList - List of player objects with their details
+ */
+function handlePlayerDataRequest(playerList) {
+ adminData.players = playerList;
+ updateStats();
updatePlayerList();
}
-// Update header statistics
+// #endregion
+
+//=============================================================================
+// #region UI UPDATES AND DISPLAY
+//=============================================================================
+
+/**
+ * Update header statistics
+ * Shows counts of online players and staff
+ */
function updateStats() {
- const onlinePlayers = adminData.players.filter(p => p.status === "online").length;
- const onlineStaff = adminData.players.filter(p => p.status === "online" && p.rank > 1).length;
-
+ const onlinePlayers = adminData.players.length;
+ const onlineStaff = adminData.players.filter(p => p.paygrade !== "E1").length;
+
document.getElementById('playerCount').textContent = onlinePlayers;
document.getElementById('staffCount').textContent = onlineStaff;
}
-// Set up filter button listeners
+/**
+ * Set up filter button listeners
+ * Configures the filter buttons and search functionality
+ */
function setupFilterListeners() {
const filterButtons = document.querySelectorAll('.filter-btn');
-
+
+ // Set up filter button click handlers
filterButtons.forEach(button => {
button.addEventListener('click', () => {
filterButtons.forEach(btn => btn.classList.remove('active'));
@@ -71,83 +163,160 @@ function setupFilterListeners() {
});
}
-// Filter players based on category and search term
+/**
+ * Filter players based on category and search term
+ *
+ * @param {string} filter - The filter category (all, staff, blufor, etc.)
+ * @param {string} searchTerm - Optional search term to filter by name
+ */
function filterPlayers(filter, searchTerm = '') {
let filteredPlayers = adminData.players;
-
+
// Apply category filter
- if (filter === 'online') {
- filteredPlayers = filteredPlayers.filter(p => p.status === 'online');
- } else if (filter === 'staff') {
- filteredPlayers = filteredPlayers.filter(p => p.rank > 1);
+ if (filter === 'staff') {
+ filteredPlayers = filteredPlayers.filter(p => p.paygrade !== "E1");
+ } else if (filter === 'blufor') {
+ filteredPlayers = filteredPlayers.filter(p => p.side === "WEST");
+ } else if (filter === 'opfor') {
+ filteredPlayers = filteredPlayers.filter(p => p.side === "EAST");
+ } else if (filter === 'independent') {
+ filteredPlayers = filteredPlayers.filter(p => p.side === "GUER");
+ } else if (filter === 'civilian') {
+ filteredPlayers = filteredPlayers.filter(p => p.side === "CIV");
}
-
+
// Apply search filter
if (searchTerm) {
const term = searchTerm.toLowerCase();
- filteredPlayers = filteredPlayers.filter(p =>
+ filteredPlayers = filteredPlayers.filter(p =>
p.name.toLowerCase().includes(term)
);
}
-
+
updatePlayerList(filteredPlayers);
}
-// Update the player list display
+/**
+ * Update the player list display
+ * Renders the filtered player list with all relevant information
+ *
+ * @param {Array} players - List of player objects to display, defaults to all players
+ */
function updatePlayerList(players = adminData.players) {
const playerList = document.getElementById('playerList');
playerList.innerHTML = players.map(player => {
- const paydayAmount = adminData.paydayAmounts[player.rank] || 1000; // Default to 1000 if rank not found
+ const paydayAmount = adminData.paydayAmounts[player.paygrade] || 1000;
+ const rankClass = getRankClass(player.paygrade);
+
return `
-
+
`}).join('');
}
-// Rank management functions
-function promotePlayer(playerId) {
- const player = adminData.players.find(p => p.id === playerId);
- if (player && player.rank < adminData.maxRank) {
- player.rank++;
- updatePlayerList();
+/**
+ * Helper function to determine rank class based on paygrade
+ * Used for styling different ranks with appropriate CSS classes
+ *
+ * @param {string} paygrade - The player's paygrade code
+ * @returns {string} CSS class name for the rank
+ */
+function getRankClass(paygrade) {
+ if (paygrade.startsWith('E')) {
+ return 'enlisted';
+ } else if (paygrade.startsWith('WO')) {
+ return 'warrant';
+ } else if (paygrade.startsWith('O') ||
+ paygrade.startsWith('1') ||
+ paygrade.startsWith('2') ||
+ paygrade.startsWith('C') ||
+ paygrade.startsWith('M')) {
+ return 'officer';
+ } else {
+ return 'enlisted'; // Default
}
}
-function demotePlayer(playerId) {
- const player = adminData.players.find(p => p.id === playerId);
- if (player && player.rank > 1) {
- player.rank--;
- updatePlayerList();
+// #endregion
+
+//=============================================================================
+// #region RANK MANAGEMENT
+//=============================================================================
+
+/**
+ * Update a player's paygrade (promote or demote)
+ *
+ * @param {string} uid - Player's unique identifier
+ * @param {boolean} isPromotion - True for promotion, false for demotion
+ */
+function updatePaygrade(uid, isPromotion) {
+ const player = adminData.players.find(p => p.uid === uid);
+ if (!player) return;
+
+ // Use the paygrades from the configuration
+ const paygrades = Object.keys(adminData.paydayAmounts);
+ paygrades.sort((a, b) => adminData.paydayAmounts[a] - adminData.paydayAmounts[b]); // Sort by payment amount
+
+ const currentIndex = paygrades.indexOf(player.paygrade);
+
+ let newPaygrade;
+ if (isPromotion && currentIndex < paygrades.length - 1) {
+ newPaygrade = paygrades[currentIndex + 1];
+ } else if (!isPromotion && currentIndex > 0) {
+ newPaygrade = paygrades[currentIndex - 1];
+ } else {
+ return; // Can't promote/demote further
}
+
+ const message = {
+ event: "UPDATE::PAYGRADE",
+ data: [uid, newPaygrade]
+ };
+
+ A3API.SendAlert(JSON.stringify(message));
+
+ // Optimistic update
+ player.paygrade = newPaygrade;
+ updatePlayerList();
}
-// Money management functions
-function openMoneyModal(playerId) {
- selectedPlayerId = playerId;
+// #endregion
+
+//=============================================================================
+// #region MONEY MANAGEMENT
+//=============================================================================
+
+/**
+ * Open the money modification modal for a player
+ *
+ * @param {string} uid - Player's unique identifier
+ */
+function openMoneyModal(uid) {
+ selectedPlayerId = uid;
const modal = document.getElementById('moneyModal');
modal.style.display = 'block';
}
+/**
+ * Close the money modification modal
+ */
function closeMoneyModal() {
const modal = document.getElementById('moneyModal');
modal.style.display = 'none';
@@ -155,39 +324,93 @@ function closeMoneyModal() {
selectedPlayerId = null;
}
+/**
+ * Give money to the selected player
+ */
function giveMoney() {
const amount = parseInt(document.getElementById('moneyAmount').value);
if (amount && selectedPlayerId) {
- const player = adminData.players.find(p => p.id === selectedPlayerId);
- if (player) {
- player.money += amount;
- updatePlayerList();
- closeMoneyModal();
- }
+ handleTransferFunds("advance", amount, selectedPlayerId);
+ closeMoneyModal();
}
}
+/**
+ * Give money to all players
+ */
+function giveAllMoney() {
+ const amount = parseInt(document.getElementById('giveAllAmount').value);
+ const message = {
+ event: "ADVANCE::ALL",
+ data: [amount]
+ }
+
+ A3API.SendAlert(JSON.stringify(message));
+
+ // Request updated player data after giving money to all players
+ setTimeout(requestPlayerData, 500); // Short delay to allow server processing
+}
+
+/**
+ * Take money from the selected player
+ */
function takeMoney() {
const amount = parseInt(document.getElementById('moneyAmount').value);
if (amount && selectedPlayerId) {
- const player = adminData.players.find(p => p.id === selectedPlayerId);
- if (player) {
- player.money = Math.max(0, player.money - amount);
- updatePlayerList();
- closeMoneyModal();
- }
+ handleTransferFunds("deduct", amount, selectedPlayerId);
+ closeMoneyModal();
}
}
-// Message system functions
-function openMessageModal(playerId) {
- selectedPlayerId = playerId;
- const player = adminData.players.find(p => p.id === playerId);
+/**
+ * Handle funds transfer for a player
+ *
+ * @param {string} condition - "advance" to give money, "deduct" to take money
+ * @param {number} amount - Amount of money to transfer
+ * @param {string} uid - Player's unique identifier
+ */
+function handleTransferFunds(condition, amount, uid) {
+ const message = {
+ event: "HANDLE::TRANSFER",
+ data: [condition, amount, uid]
+ };
+
+ A3API.SendAlert(JSON.stringify(message));
+
+ // Optimistic update
+ const player = adminData.players.find(p => p.uid === uid);
+ if (player) {
+ if (condition === "advance") {
+ player.funds = parseInt(player.funds) + amount;
+ } else if (condition === "deduct") {
+ player.funds = Math.max(0, parseInt(player.funds) - amount);
+ }
+ updatePlayerList();
+ }
+}
+
+// #endregion
+
+//=============================================================================
+// #region MESSAGE SYSTEM
+//=============================================================================
+
+/**
+ * Open the message modal for a player
+ *
+ * @param {string} uid - Player's unique identifier
+ * @param {string} playerName - Player's name for display
+ */
+function openMessageModal(uid, playerName) {
+ selectedPlayerId = uid;
const modal = document.getElementById('messageModal');
- document.getElementById('messagePlayerName').textContent = player.name;
+ document.getElementById('messagePlayerName').textContent = playerName;
modal.style.display = 'block';
}
+/**
+ * Close the message modal
+ */
function closeMessageModal() {
const modal = document.getElementById('messageModal');
modal.style.display = 'none';
@@ -195,35 +418,71 @@ function closeMessageModal() {
selectedPlayerId = null;
}
+/**
+ * Send a message to the selected player
+ */
function sendPlayerMessage() {
const message = document.getElementById('messageInput').value;
if (message && selectedPlayerId) {
- const player = adminData.players.find(p => p.id === selectedPlayerId);
- if (player) {
- console.log(`Message sent to ${player.name}: ${message}`);
- closeMessageModal();
- }
+ const messageData = {
+ event: "SEND::MESSAGE",
+ data: [selectedPlayerId, message]
+ };
+
+ A3API.SendAlert(JSON.stringify(messageData));
+ closeMessageModal();
}
}
+/**
+ * Broadcast a message to all players
+ */
function broadcastMessage() {
const message = document.getElementById('broadcastMessage').value;
if (message) {
- console.log(`Broadcasting message to all players: ${message}`);
+ const messageData = {
+ event: "BROADCAST::MESSAGE",
+ data: ["", message]
+ };
+
+ A3API.SendAlert(JSON.stringify(messageData));
document.getElementById('broadcastMessage').value = '';
}
}
-// Global actions
+// #endregion
+
+//=============================================================================
+// #region GLOBAL ACTIONS
+//=============================================================================
+
+/**
+ * Trigger a payday for all players
+ */
function Payday() {
- const amount = parseInt(document.getElementById('paydayAmount').value);
- if (amount) {
- adminData.players.forEach(player => {
- player.money += amount;
- });
- updatePlayerList();
- }
+ const message = {
+ event: "HANDLE::PAYDAY",
+ data: []
+ };
+
+ A3API.SendAlert(JSON.stringify(message));
+
+ // Request updated player data after payday
+ setTimeout(requestPlayerData, 500); // Short delay to allow server processing
}
-// Initialize when DOM is loaded
-document.addEventListener('DOMContentLoaded', initializeAdmin);
+// #endregion
+
+//=============================================================================
+// #region EVENT LISTENERS
+//=============================================================================
+
+/**
+ * Initialize when DOM is loaded
+ */
+document.addEventListener('DOMContentLoaded', () => {
+ initializeAdmin();
+ setupRefreshTimer();
+});
+
+// #endregion
\ No newline at end of file
diff --git a/addons/admin/ui/_site/styles.css b/addons/admin/ui/_site/styles.css
index c667f29..e3fb16c 100644
--- a/addons/admin/ui/_site/styles.css
+++ b/addons/admin/ui/_site/styles.css
@@ -1,30 +1,46 @@
+/* =============================================================================
+ BASE STYLES AND VARIABLES
+ ============================================================================= */
+
+/* Reset styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
+/* Color variables and theme configuration */
:root {
+ /* Primary colors */
--primary-color: #3b82f6;
--primary-hover: #2563eb;
--secondary-color: #1e293b;
+
+ /* Background colors */
--background-color: #f1f5f9;
--card-background: #ffffff;
+ --header-bg: #1e293b;
+ --tile-hover: #f8fafc;
+
+ /* Text colors */
--text-primary: #0f172a;
--text-secondary: #475569;
- --border-color: #e2e8f0;
+ --header-text: #f8fafc;
+
+ /* Status colors */
--success-color: #16a34a;
--success-hover: #15803d;
--error-color: #dc2626;
--error-hover: #b91c1c;
--warning-color: #f59e0b;
--warning-hover: #d97706;
- --header-bg: #1e293b;
- --header-text: #f8fafc;
- --tile-hover: #f8fafc;
+
+ /* Utility colors */
+ --border-color: #e2e8f0;
--shadow-color: rgba(0, 0, 0, 0.1);
}
+/* Base body styles */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
@@ -32,6 +48,11 @@ body {
color: var(--text-primary);
}
+/* =============================================================================
+ LAYOUT COMPONENTS
+ ============================================================================= */
+
+/* Main container */
.container {
max-width: 1280px;
margin: 0 auto;
@@ -39,28 +60,30 @@ body {
margin-bottom: 1.5rem;
}
+/* Header styles */
header {
background-color: var(--header-bg);
color: var(--header-text);
padding: 1rem 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+
+ & .header-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 0 1rem;
+ }
+
+ & h1 {
+ font-size: 1.75rem;
+ font-weight: 600;
+ letter-spacing: -0.025em;
+ }
}
-.header-content {
- display: flex;
- justify-content: space-between;
- align-items: center;
- max-width: 1280px;
- margin: 0 auto;
- padding: 0 1rem;
-}
-
-header h1 {
- font-size: 1.75rem;
- font-weight: 600;
- letter-spacing: -0.025em;
-}
-
+/* Admin stats in header */
.admin-stats {
display: flex;
align-items: center;
@@ -68,58 +91,59 @@ header h1 {
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
+
+ & .stat-item {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.5rem 0.75rem;
+ border-radius: 6px;
+ transition: all 0.2s ease-in-out;
+ min-width: 140px;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.1);
+ }
+ }
+
+ & .stat-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ font-size: 1rem;
+ }
+
+ & .stat-info {
+ display: flex;
+ flex-direction: column;
+ gap: 0.125rem;
+ }
+
+ & .stat-label {
+ font-size: 0.75rem;
+ color: rgba(255, 255, 255, 0.7);
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.025em;
+ }
+
+ & .stat-value {
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--header-text);
+ }
+
+ & .stat-divider {
+ width: 1px;
+ height: 24px;
+ background: rgba(255, 255, 255, 0.1);
+ margin: 0 0.25rem;
+ }
}
-.stat-item {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- padding: 0.5rem 0.75rem;
- border-radius: 6px;
- transition: all 0.2s ease-in-out;
- min-width: 140px;
-}
-
-.stat-item:hover {
- background: rgba(255, 255, 255, 0.1);
-}
-
-.stat-icon {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- font-size: 1rem;
-}
-
-.stat-info {
- display: flex;
- flex-direction: column;
- gap: 0.125rem;
-}
-
-.stat-label {
- font-size: 0.75rem;
- color: rgba(255, 255, 255, 0.7);
- font-weight: 500;
- text-transform: uppercase;
- letter-spacing: 0.025em;
-}
-
-.stat-value {
- font-size: 0.875rem;
- font-weight: 600;
- color: var(--header-text);
-}
-
-.stat-divider {
- width: 1px;
- height: 24px;
- background: rgba(255, 255, 255, 0.1);
- margin: 0 0.25rem;
-}
-
+/* Grid layout for sections */
.sections-grid {
display: grid;
grid-template-columns: 1fr;
@@ -133,6 +157,11 @@ header h1 {
margin-top: 1.5rem;
}
+/* =============================================================================
+ ADMIN SECTION COMPONENTS
+ ============================================================================= */
+
+/* Admin section cards */
.admin-section {
background-color: var(--card-background);
border-radius: 12px;
@@ -146,22 +175,23 @@ header h1 {
transition: all 0.3s ease-in-out;
height: auto;
max-height: calc(100vw / 3);
+
+ &:hover {
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+ transform: translateY(-4px);
+ }
+
+ &:active {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ }
+
+ &.square-ratio {
+ aspect-ratio: 1 / 1;
+ }
}
-.admin-section:hover {
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
- transform: translateY(-4px);
-}
-
-.admin-section:active {
- transform: translateY(-2px);
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
-}
-
-.admin-section.square-ratio {
- aspect-ratio: 1 / 1;
-}
-
+/* Player list section */
.player-list-section {
grid-column: span 1;
height: auto;
@@ -174,27 +204,28 @@ header h1 {
flex: 1;
padding-right: 0.5rem;
margin-right: -0.5rem;
+
+ /* Customize scrollbar for webkit browsers */
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: transparent;
+ margin: 0.5rem;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background-color: rgba(0, 0, 0, 0.1);
+ border-radius: 3px;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.2);
+ }
+ }
}
-/* Customize scrollbar for webkit browsers */
-.player-list::-webkit-scrollbar {
- width: 6px;
-}
-
-.player-list::-webkit-scrollbar-track {
- background: transparent;
- margin: 0.5rem;
-}
-
-.player-list::-webkit-scrollbar-thumb {
- background-color: rgba(0, 0, 0, 0.1);
- border-radius: 3px;
-}
-
-.player-list::-webkit-scrollbar-thumb:hover {
- background-color: rgba(0, 0, 0, 0.2);
-}
-
+/* Player item in the list */
.player-item {
display: flex;
justify-content: space-between;
@@ -206,30 +237,42 @@ header h1 {
border: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
margin-bottom: 0.5rem;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ &:hover {
+ background-color: var(--tile-hover);
+ box-shadow: 0 4px 6px var(--shadow-color);
+ transform: translateY(-2px);
+ }
+
+ & .player-info {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ flex: 1;
+ }
+
+ & .player-name {
+ font-weight: 600;
+ color: var(--text-primary);
+ }
+
+ & .player-money {
+ font-size: 0.875rem;
+ color: var(--success-color);
+ font-weight: 500;
+ }
+
+ & .player-actions {
+ display: flex;
+ gap: 0.5rem;
+ }
}
-.player-item:last-child {
- margin-bottom: 0;
-}
-
-.player-item:hover {
- background-color: var(--tile-hover);
- box-shadow: 0 4px 6px var(--shadow-color);
- transform: translateY(-2px);
-}
-
-.player-info {
- display: flex;
- align-items: center;
- gap: 1rem;
- flex: 1;
-}
-
-.player-name {
- font-weight: 600;
- color: var(--text-primary);
-}
-
+/* Player role badges */
.player-role {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
@@ -254,17 +297,11 @@ header h1 {
color: white;
}
-.player-money {
- font-size: 0.875rem;
- color: var(--success-color);
- font-weight: 500;
-}
-
-.player-actions {
- display: flex;
- gap: 0.5rem;
-}
+/* =============================================================================
+ BUTTONS AND INTERACTIVE ELEMENTS
+ ============================================================================= */
+/* Action buttons */
.action-btn {
padding: 0.5rem 1rem;
border-radius: 6px;
@@ -278,30 +315,31 @@ header h1 {
.promote-btn {
background-color: #22c55e;
color: white;
-}
-
-.promote-btn:hover {
- background-color: #16a34a;
+
+ &:hover {
+ background-color: #16a34a;
+ }
}
.demote-btn {
background-color: #ef4444;
color: white;
-}
-
-.demote-btn:hover {
- background-color: #dc2626;
+
+ &:hover {
+ background-color: #dc2626;
+ }
}
.message-btn {
background-color: #3b82f6;
color: white;
+
+ &:hover {
+ background-color: #2563eb;
+ }
}
-.message-btn:hover {
- background-color: #2563eb;
-}
-
+/* Search and filter components */
.search-bar {
display: flex;
flex-direction: column;
@@ -317,49 +355,81 @@ header h1 {
color: var(--text-primary);
background-color: var(--card-background);
transition: all 0.2s ease-in-out;
+
+ &:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+ }
+
+ &:hover {
+ border-color: var(--primary-color);
+ }
}
-.search-input:focus {
- outline: none;
- border-color: var(--primary-color);
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+.filter-bar {
+ display: flex;
+ gap: 0.5rem;
}
-.search-input:hover {
- border-color: var(--primary-color);
+.filter-btn {
+ padding: 0.5rem 0.75rem;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ cursor: pointer;
+ background-color: var(--card-background);
+ color: var(--text-secondary);
+ font-weight: 500;
+ font-size: 0.875rem;
+ transition: all 0.2s ease-in-out;
+
+ &:hover {
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+ }
+
+ &.active {
+ background-color: var(--primary-color);
+ color: white;
+ border-color: var(--primary-color);
+ }
}
+/* =============================================================================
+ FORMS AND INPUTS
+ ============================================================================= */
+
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
-}
-
-.form-group label {
- font-weight: 500;
- color: var(--text-secondary);
- font-size: 0.875rem;
-}
-
-.form-group input {
- padding: 0.75rem 1rem;
- border: 1px solid var(--border-color);
- border-radius: 8px;
- font-size: 1rem;
- color: var(--text-primary);
- background-color: var(--card-background);
- transition: all 0.2s ease-in-out;
-}
-
-.form-group input:focus {
- outline: none;
- border-color: var(--primary-color);
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
-}
-
-.form-group input:hover {
- border-color: var(--primary-color);
+
+ & label {
+ font-weight: 500;
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+ }
+
+ & input {
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ font-size: 1rem;
+ color: var(--text-primary);
+ background-color: var(--card-background);
+ transition: all 0.2s ease-in-out;
+
+ &:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+ }
+
+ &:hover {
+ border-color: var(--primary-color);
+ }
+ }
}
.submit-btn {
@@ -374,21 +444,25 @@ header h1 {
transition: all 0.2s ease-in-out;
opacity: 0.9;
margin-top: auto;
+
+ &:hover {
+ background-color: var(--primary-hover);
+ opacity: 1;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ }
+
+ &:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
}
-.submit-btn:hover {
- background-color: var(--primary-hover);
- opacity: 1;
- transform: translateY(-2px);
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
-}
+/* =============================================================================
+ MODALS
+ ============================================================================= */
-.submit-btn:active {
- transform: translateY(0);
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-
-.modal {
+ .modal {
display: none;
position: fixed;
top: 0;
@@ -417,12 +491,12 @@ header h1 {
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
-}
-
-.modal-header h2 {
- font-size: 1.25rem;
- color: var(--text-primary);
- font-weight: 600;
+
+ & h2 {
+ font-size: 1.25rem;
+ color: var(--text-primary);
+ font-weight: 600;
+ }
}
.close-modal {
@@ -432,11 +506,15 @@ header h1 {
cursor: pointer;
color: var(--text-secondary);
transition: color 0.2s ease-in-out;
+
+ &:hover {
+ color: var(--text-primary);
+ }
}
-.close-modal:hover {
- color: var(--text-primary);
-}
+/* =============================================================================
+ BADGES AND STATUS INDICATORS
+ ============================================================================= */
.badge {
padding: 0.25rem 0.5rem;
@@ -447,36 +525,9 @@ header h1 {
letter-spacing: 0.025em;
}
-.filter-bar {
- display: flex;
- gap: 0.5rem;
-}
-
-.filter-btn {
- padding: 0.5rem 0.75rem;
- border: 1px solid var(--border-color);
- border-radius: 6px;
- cursor: pointer;
- background-color: var(--card-background);
- color: var(--text-secondary);
- font-weight: 500;
- font-size: 0.875rem;
- transition: all 0.2s ease-in-out;
-}
-
-.filter-btn:hover {
- border-color: var(--primary-color);
- color: var(--primary-color);
-}
-
-.filter-btn.active {
- background-color: var(--primary-color);
- color: white;
- border-color: var(--primary-color);
-}
-
-/* Rank badges */
-.player-rank {
+/* Rank & Side badges */
+.player-rank,
+.player-side {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
@@ -485,37 +536,44 @@ header h1 {
letter-spacing: 0.025em;
}
-.rank-1 {
+/* Rank styling based on type */
+.rank-enlisted {
background-color: #e2e8f0;
color: #475569;
}
-.rank-2 {
+.rank-warrant {
background-color: #bfdbfe;
color: #1e40af;
}
-.rank-3 {
- background-color: #93c5fd;
- color: #1e40af;
-}
-
-.rank-4 {
- background-color: #60a5fa;
- color: #1e40af;
-}
-
-.rank-5 {
+.rank-officer {
background-color: #3b82f6;
color: #ffffff;
}
-.payday-description {
- font-size: 0.875rem;
- color: var(--text-secondary);
- margin: 0.5rem 0;
+/* Side indicators */
+.side-west {
+ background-color: #3b82f6;
+ color: white;
}
+.side-east {
+ background-color: #ef4444;
+ color: white;
+}
+
+.side-guer {
+ background-color: #22c55e;
+ color: white;
+}
+
+.side-civ {
+ background-color: #a855f7;
+ color: white;
+}
+
+/* Player payday indicator */
.player-payday {
font-size: 0.75rem;
color: var(--success-color);
@@ -525,3 +583,64 @@ header h1 {
border-radius: 4px;
margin-left: 0.5rem;
}
+
+/* Description text */
+.payday-description {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+ margin: 0.5rem 0;
+}
+
+/* =============================================================================
+ RESPONSIVE ADJUSTMENTS
+ ============================================================================= */
+
+/* Adjustments for smaller screens */
+@media (max-width: 768px) {
+ .action-sections {
+ grid-template-columns: 1fr;
+ }
+
+ .player-item {
+ flex-direction: column;
+ align-items: flex-start;
+
+ & .player-info {
+ margin-bottom: 1rem;
+ flex-wrap: wrap;
+ }
+
+ & .player-actions {
+ width: 100%;
+ justify-content: space-between;
+ }
+ }
+
+ .filter-bar {
+ overflow-x: auto;
+ padding-bottom: 0.5rem;
+ }
+
+ .modal-content {
+ min-width: 90%;
+ max-width: 90%;
+ }
+}
+
+/* Adjustments for very small screens */
+@media (max-width: 480px) {
+ .admin-stats {
+ flex-direction: column;
+
+ & .stat-divider {
+ width: 80%;
+ height: 1px;
+ margin: 0.25rem 0;
+ }
+ }
+
+ .action-btn {
+ padding: 0.5rem;
+ font-size: 0.75rem;
+ }
+}
\ No newline at end of file
diff --git a/addons/ambient/README.md b/addons/ambient/README.md
new file mode 100644
index 0000000..4d206a2
--- /dev/null
+++ b/addons/ambient/README.md
@@ -0,0 +1,74 @@
+# Forge Ambient Module
+
+## Overview
+The Ambient module provides environmental sound management for the Forge client system. It enables the creation and management of ambient sound effects in the game environment, enhancing the immersive experience for players.
+
+## Dependencies
+- forge_client_main
+
+## Authors
+- J. Schmidt
+- Creedcoder
+- IDSolutions
+
+## Features
+
+### Sound Management
+1. **Ambient Sound System** (`fnc_ambientSound.sqf`)
+ - Creates and manages sound sources in the game world
+ - Handles sound effect playback and cleanup
+ - Supports timed and continuous sound effects
+
+2. **Sound Source Features**
+ - Dynamic sound source creation
+ - Position-based sound placement
+ - Automatic cleanup of sound sources
+ - Support for various sound types and durations
+
+3. **Sound Control**
+ - Lifecycle management of sound sources
+ - Automatic cleanup when source is destroyed
+ - Configurable sound duration
+ - Position tracking for moving sound sources
+
+## Event Handlers
+The module uses several event handlers for initialization and execution:
+- `XEH_preInit.sqf`: Pre-initialization setup
+- `XEH_postInit.sqf`: Post-initialization tasks
+- `XEH_preStart.sqf`: Pre-start configuration
+- `XEH_postInit_client.sqf`: Client-specific post-initialization
+- `XEH_preInit_server.sqf`: Server-specific pre-initialization
+
+## Usage
+To use the ambient module:
+1. Ensure the module is properly loaded in your mission
+2. Create sound sources using the ambient sound function:
+ ```sqf
+ [source, "sfx_sound_name"] spawn forge_client_ambient_fnc_ambientSound
+ ```
+3. Optionally specify a duration for temporary sounds:
+ ```sqf
+ [source, "sfx_sound_name", duration] spawn forge_client_ambient_fnc_ambientSound
+ ```
+
+## Parameters
+The ambient sound function accepts the following parameters:
+1. `source`: The object or position where the sound will be played
+2. `sfx`: The name of the sound effect to play
+3. `time` (optional): Duration in seconds before the sound is removed
+
+## Debugging
+Debug mode can be enabled by uncommenting the following in `script_component.hpp`:
+```cpp
+#define DEBUG_MODE_FULL
+```
+
+## Version Information
+Version information is managed through the main Forge client system configuration.
+
+## Technical Details
+- Supports both object-attached and position-based sound sources
+- Automatic cleanup of sound sources when source is destroyed
+- Configurable sound duration for temporary effects
+- Efficient sound source management
+- Client-side sound processing
\ No newline at end of file
diff --git a/addons/ambient/functions/fnc_ambientSound.sqf b/addons/ambient/functions/fnc_ambientSound.sqf
index 4fa77b2..0fbf8ee 100644
--- a/addons/ambient/functions/fnc_ambientSound.sqf
+++ b/addons/ambient/functions/fnc_ambientSound.sqf
@@ -1,11 +1,8 @@
#include "..\script_component.hpp"
/*
- * Function: forge_client_ambient_fnc_ambientSound
- * Author: J.Schmidt
- *
- * [Description]
- * Create a sound source and play an ambient sfx sound.
+ * Author: IDSolutions
+ * Create a sound source and play an ambient sfx sound
*
* Arguments:
* 0: The sound source
${player.name}
- Rank ${player.rank}
- $${player.money.toLocaleString()}
- Payday: $${paydayAmount.toLocaleString()}
+ ${player.paygrade}
+ ${parseInt(player.funds).toLocaleString()}
+ Payday: ${paydayAmount.toLocaleString()}
+ ${player.side}
- ${player.rank < adminData.maxRank ? `
-
- ` : ''}
- ${player.rank > 1 ? `
-
- ` : ''}
-
-
+
+
+
+