Initial commit

This commit is contained in:
Jacob Schmidt 2025-11-22 22:43:37 -06:00
commit 7ce6c0bcad
299 changed files with 23112 additions and 0 deletions

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
end_of_line = crlf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.yml]
indent_size = 2

13
.gitattributes vendored Normal file
View File

@ -0,0 +1,13 @@
# Sources
*.cpp text diff=cpp linguist-language=cpp
*.hpp text diff=cpp linguist-language=cpp
*.rhai text diff=rust linguist-language=rust
*.png binary
*.jpg binary
*.paa binary
# Linguistics
# Exclude included files and examples from stats
include/* linguist-vendored
extra/* linguist-vendored

12
.gitea/CONTRIBUTING.md Normal file
View File

@ -0,0 +1,12 @@
# Contributing Setup & Guidelines
## Setting up the Development Environment
### 1. Clone the repository from GitHub
### 2. Install HEMTT
The latest version of HEMTT can be installed by running:
```cmd
winget install hemtt
```
## Coding Guidelines
This mod follows the same coding guidelines as the ACE3 mod, which can be found [here](https://ace3.acemod.org/wiki/development/coding-guidelines).

View File

@ -0,0 +1,25 @@
---
name: Bug report
about: Create a bug report to help us improve
title: ''
labels: kind/bug
---
## Describe the bug
A clear and concise description of what the bug is.
## To reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
## Expected behavior
A clear and concise description of what you expected to happen.
## Attachments
If applicable, add screenshots or RPT logs to help explain your problem.
## Additional context
Add any other context about the problem here.

View File

@ -0,0 +1,15 @@
---
name: Feature Request
about: Suggest a feature to be added
title: ''
labels: kind/feature-request
---
## Describe the feature that you would like
A clear and concise description of the feature you'd want.
## Possible alternatives
Possible alternatives to your suggestion.
## Additional context
Add any other context about the feature here.

View File

@ -0,0 +1,12 @@
**When merged this pull request will:**
- Describe what this pull request will do
- Each change in a separate line
### Important
- [ ] If the contribution affects [the documentation](../docs), please include your changes in this pull request.
- [ ] [Development Guidelines](https://github.com/IDSolutions/MOD_REPO/blob/main/.github/CONTRIBUTING.md) are read, understood and applied.
- [ ] Title of this PR uses our standard template `Component - Add|Fix|Improve|Change|Make|Remove {changes}`.
<!-- Known issues that need to be addressed -->
### Known Issues
- [ ] Issue

View File

@ -0,0 +1 @@

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Rust
/target/
**/*.rs.bk
*.pdb
# Cargo
Cargo.lock
debug/
target/
# Build artifacts
*.exe
*.dll
*.so
*.dylib
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db

131
Architecture_Diagram.md Normal file
View File

@ -0,0 +1,131 @@
# Forge Architecture & Data Flow Diagram
## 🏗️ **System Architecture Overview**
```mermaid
graph TD
subgraph ForgeSystem [FORGE SYSTEM]
subgraph Clients [Clients #40;Read-Only#41;]
ClientA[CLIENT A]
ClientB[CLIENT B]
ClientN[CLIENT N]
subgraph OptimisticCache [Optimistic Cache]
ActorObj[Actor Object<br/>- loadout<br/>- position<br/>- stats]
end
ClientA --- OptimisticCache
ClientB --- OptimisticCache
ClientN --- OptimisticCache
end
subgraph Server [ArmA 3 SERVER #40;Hot Cache#41;]
ActorRegistry["GVAR(ActorRegistry)<br/>In-Memory HashMap<br/>UID -> {loadout, position, stats...}"]
SessionMgmt[Session Management<br/>- Token Generation<br/>- UID Resolution<br/>- Player State]
end
subgraph Rust [EXTENSION #40;Cold Storage#41;]
ConnPool["Connection Pool<br/>(bb8-redis)<br/>2-10 connections"]
RedisOps[Redis Operations<br/>- actor_get/set/update<br/>- Async I/O]
end
subgraph Redis [DATABASE #40;Saved to Disc#41;]
ActorDataStore[Actor Data Store<br/>actor:UID -> JSON]
Modules[Additional Modules<br/>garage, locker, bank, org]
end
Clients -->|Event Driven<br/>#40;CBA A3 Events#41;| Server
Server -->|Extension Calls<br/>#40;Rust FFI#41;| Rust
Rust -->|Redis Protocol<br/>#40;bb8-redis#41;| Redis
end
```
## 🔄 **Data Flow Sequence**
### **1. Player Connection & Initial Data Load**
```mermaid
sequenceDiagram
participant Client
participant Server as Server (Hot Cache)
participant Extension as Extension (Cold Storage)
participant Redis as Redis (Database)
Note over Client, Redis: 1. Player Connection & Initial Data Load
Client->>Server: 1. Connect
Client->>Server: 2. Request Actor Data
Server->>Server: 3. Check Cache (Cache Miss)
Server->>Extension: 4. Extension Call
Extension->>Redis: 5. Redis Query
Redis-->>Extension: 6. JSON Data
Extension-->>Server: 7. Actor Data
Server->>Server: 8. Store in Hot Cache
Server-->>Client: 9. Secure Response
Client->>Client: 10. Update Local Cache
```
### **2. Subsequent Data Access (Cache Hit)**
```mermaid
sequenceDiagram
participant Client
participant Server as Server (Hot Cache)
participant Extension as Extension (Cold Storage)
participant Redis as Redis (Database)
Note over Client, Redis: 2. Subsequent Data Access (Cache Hit)
Client->>Server: 1. Request Actor Data
Server->>Server: 2. Check Cache (Cache Hit!)
Server-->>Client: 3. Instant Response
Client->>Client: 4. Update Local Cache
```
### **3. Data Update (Write-Through)**
```mermaid
sequenceDiagram
participant Client
participant Server as Server (Hot Cache)
participant Extension as Extension (Cold Storage)
participant Redis as Redis (Database)
Note over Client, Redis: 3. Data Update (Write-Through)
Client->>Server: 1. Action (Move, etc)
Server->>Server: 2. Validate & Update Cache
Server->>Extension: 3. Persist to Database
Extension->>Redis: 4. Redis Update
Redis-->>Extension: 5. Confirmation
Extension-->>Server: 6. Success
Server-->>Client: 7. Sync to All Clients
```
## 🚀 **Performance Characteristics**
### **Access Times**
- **Hot Cache (Server)**: `< 1ms` (HashMap lookup)
- **Cold Storage (Redis)**: `1-5ms` (Network + Redis)
- **Client Cache**: `< 0.1ms` (Local object access)
### **Cache Hit Ratios**
- **Hot Cache**: `~95%` (Active players)
- **Cold Storage**: `~5%` (New connections, cache misses)
### **Memory Usage**
- **Server Registry**: `~1KB per active player`
- **Client Cache**: `~500B per player object`
- **Redis**: `~2KB per player (persistent)`
## 🔒 **Security & Session Management**
```mermaid
flowchart TD
subgraph SessionMgmt [SERVER-SIDE #40;Session MGT#41;]
Conn[Player Connection] --> Token[Session Token Generation<br/>#40;Generated on server#41;]
Token --> UID[UID Resolution<br/>#40;Steam UID mapping#41;]
UID --> State[Player State Tracking<br/>#40;Tracked in ActorRegistry#41;]
State --> Access[Data Access Authorized<br/>#40;Authorized via session#41;]
end
```

18
Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[workspace]
members = [
"arma/server/extension",
"lib/models",
"lib/repositories",
"lib/services",
"lib/shared",
]
resolver = "3"
[workspace.dependencies]
arma-rs = { version = "1.11.15", features = ["chrono", "serde_json", "uuid"] }
chrono = "0.4.42"
redis = "1.0.0-rc.1"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
tokio = { version = "1.47.1", features = ["full"] }
uuid = { version = "1.18.1", features = ["v4"] }

119
LICENSE.md Normal file
View File

@ -0,0 +1,119 @@
![APL-SA](https://www.bohemia.net/assets/img/licenses/APL-SA.png)
## Brief summary of this Licence
PLEASE, NOTE THAT THIS SUMMARY HAS NO LEGAL EFFECT AND IS ONLY OF AN INFORMATORY NATURE DESIGNED FOR YOU TO GET THE BASIC INFORMATION ABOUT THE CONTENT OF THIS LICENCE. THE ONLY LEGALLY BINDING PROVISIONS ARE THOSE IN THE ORIGINAL AND FULL TEXT OF THIS LICENCE.
With this licence you are free to adapt (i.e. modify, rework or update) and share (i.e. copy, distribute or transmit) the material under the following conditions:
* **Attribution** - You must attribute the material in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the material).
* **Noncommercial** - You may not use this material for any commercial purposes.
* **Arma Only** - You may not convert or adapt this material to be used in other games than Arma.
* **Share Alike** - If you adapt, or build upon this material, you may distribute the resulting material only under the same license.
---
# Full version of licence
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Arma Public License - Share Alike ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
### Section 1 Definitions
1. **Adapted Material** means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
2. **Adapter's License** means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
3. **ArmaOnly** means primarily intended for or directed towards the use in any of existing and future Arma games, including but not limited to Arma: Cold War Assault, Arma, Arma 2 and Arma 3 and its official sequels and expansion packs.
4. **Arma Public Share Alike Compatible License** means a license listed at [https://www.bohemia.net/community/licenses](https://www.bohemia.net/community/licenses) as essentially the equivalent of this Public License.
5. **Copyright and Similar Rights** means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
6. **Effective Technological Measures** means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
7. **Exceptions and Limitations** means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
8. **Licensed Material** means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
9. **Licensed Rights** means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
10. **Licensor** means the individual(s) or entity(ies) granting rights under this Public License.
11. **NonCommercial** means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange.
12. **Share** means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
13. **Sui Generis Database Rights** means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
14. **You** means the individual or entity exercising the Licensed Rights under this Public License. **Your** has a corresponding meaning.
### Section 2 Scope
1. **License grant**
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
1. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial and ArmaOnly purposes only; and
2. produce, reproduce, and Share Adapted Material for NonCommercial and ArmaOnly purposes only.
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
3. Term. The term of this Public License is specified in Section 6(a).
4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
5. Downstream recipients.
1. Offer from the Licensor Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
2. Additional offer from the Licensor Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapters License You apply.
3. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(a)(i).
2. **Other rights**
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this Public License.
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial and ArmaOnly purposes.
### Section 3 License Conditions
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
1. **Attribution**
1. If You Share the Licensed Material (including in modified form), You must:
1. retain the following if it is supplied by the Licensor with the Licensed Material:
1. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
2. a copyright notice;
3. a notice that refers to this Public License;
4. a notice that refers to the disclaimer of warranties;
5. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
2. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
3. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(a) to the extent reasonably practicable.
2. **ShareAlike**
In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
1. The Adapters License You apply must be this Public License, or an Arma Public Share Alike Compatible License.
2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
### Section 4 Sui Generis Database Rights
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
1. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial and ArmaOnly purposes only;
2. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
3. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
### Section 5 Disclaimer of Warranties and Limitation of Liability
1. **Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.**
2. **To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.**
3. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
### Section 6 Term and Termination
1. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
2. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
3. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
4. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
### Section 7 Other Terms and Conditions
1. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
2. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
### Section 8 Interpretation
1. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
2. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
3. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
4. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
### Bohemia Interactive Notices
1. Bohemia Interactive a.s. is not a party to this License, and makes no warranty whatsoever in connection with the Licensed Material. Bohemia Interactive a.s. will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, Bohemia Interactive a.s. may elect to apply the Public License to material it publishes and in those instances it becomes the "Licensor".
2. Except for the limited purpose of indicating to the public that the Licensed Material is shared under this Public License, Bohemia Interactive a.s. does not authorize the use by either party of the trademarks "Arma", "Bohemia Interactive" or any related trademark or logo of Arma or Bohemia Interactive without the prior written consent of Bohemia Interactive a.s.

294
README.md Normal file
View File

@ -0,0 +1,294 @@
# Forge Framework
**Forge** is a high-performance, production-ready framework for Arma 3 persistent game servers, built with Rust and Redis for optimal performance and reliability.
## Overview
Forge provides a complete solution for managing persistent player data, organizations, and game state in Arma 3 multiplayer environments. It combines the performance of Rust with the flexibility of Redis to deliver sub-millisecond response times while maintaining data consistency across server restarts.
### Key Features
- **🚀 High Performance**: Sub-millisecond data access through intelligent caching
- **🔒 Data Integrity**: Strict validation and type safety at every layer
- **🏗️ Clean Architecture**: Layered design following SOLID principles
- **📦 Modular Design**: Easy to extend with new entities and features
- **🔄 Real-time Sync**: Automatic state synchronization across all clients
- **💾 Persistent Storage**: Redis-backed storage with automatic failover
- **🧪 Testable**: Mock-friendly architecture for comprehensive testing
## Architecture
Forge follows a **layered architecture** pattern:
```mermaid
graph TD
Extension[Extension Layer<br/>ArmA 3 Interface <---> Rust]
Services[Services Layer<br/>#40;Business Logic#41;]
Repositories[Repositories Layer<br/>#40;Data Persistence#41;]
Models[Models Layer<br/>#40;Data Structures & Validation#41;]
Extension --> Services
Services --> Repositories
Repositories --> Models
```
**Communication Flow**:
- **Clients** → Use events (`CBA_Events`) to communicate with server
- **Server** → Calls Rust extension via `callExtension`
- **Extension** → Manages Redis connection pool and data operations
For detailed architecture information, see [Diagram](Architecture_Diagram.md).
## Project Structure
```
forge/
├── arma/
│ ├── client/ # Client-side SQF mod
│ │ ├── addons/
│ │ │ ├── main/ # Core initialization & config
│ │ │ ├── common/ # Shared utilities & helpers
│ │ │ ├── actor/ # Actor/player UI, class & events
│ │ │ ├── org/ # Organization UI, class & events
│ │ │ └── bank/ # Banking UI, class & events
│ │ ├── include/ # Header files
│ │ └── tools/ # Build tools
│ ├── server/
│ │ ├── addons/
│ │ │ ├── main/ # Core initialization & config
│ │ │ ├── common/ # Shared utilities & helpers
│ │ │ ├── actor/ # Actor/player Registry, Store & events
│ │ │ └── org/ # Organization Registry, Store & events
│ │ ├── include/ # Header files
│ │ ├── tools/ # Build tools
│ │ └── extension/ # Rust extension (Arma 3 interface)
│ │ ├── src/
│ │ │ ├── actor.rs # Actor/player commands
│ │ │ ├── org.rs # Organization commands
│ │ │ ├── redis/ # Redis operations module
│ │ │ └── adapters/ # Repository adapters
│ │ └── README.md
├── lib/
│ ├── models/ # Data structures & validation
│ ├── repositories/ # Data persistence layer
│ ├── services/ # Business logic layer
│ ├── shared/ # Common utilities & traits
│ └── README.md
└── FORGE_Architecture_Diagram.md
```
## Quick Start
### Prerequisites
- Rust 1.70+ with `cargo`
- Redis 6.0+
- HEMTT
1. Clone the repository from Gitea
2. Install HEMTT
The latest version of HEMTT can be installed by running:
```cmd
winget install hemtt
```
### Coding Guidelines
This mod follows the same coding guidelines as the ACE3 mod, which can be found [here](https://ace3.acemod.org/wiki/development/coding-guidelines).
### Building the Extension
```bash
# Build for release
cargo build --release
# The compiled extension will be at:
# target/release/forge_server.dll (Windows)
# target/release/forge_server.so (Linux)
```
### Configuration
Create `@forge_server/config.toml`:
```toml
[redis]
host = "127.0.0.1"
port = 6379
password = "" # Optional
max_connections = 10
min_connections = 2
idle_timeout = 300
```
### SQF Usage
```sqf
// Create an actor
private _data = createHashMapFromArray [
["name", "John Doe"],
["bank", 1000],
["level", 1]
];
private _result = "forge_server" callExtension ["actor:create", [getPlayerUID player, toJSON _data]];
// Get actor data
private _result = "forge_server" callExtension ["actor:get", [getPlayerUID player]];
private _actorData = fromJSON (_result select 0);
// Update actor
private _update = createHashMapFromArray [["bank", 1500]];
"forge_server" callExtension ["actor:update", [getPlayerUID player, toJSON _update]];
```
## Core Modules
### Models
Defines strict data structures with built-in validation:
- `Actor`: Player data (stats, inventory, position)
- `Org`: Organization/clan data (members, roles, metadata)
[Documentation](lib/models/README.md)
### Repositories
Manages data persistence with Redis:
- Hash-based storage for structured data
- Set-based storage for collections
- Generic over Redis client implementations
[Documentation](lib/repositories/README.md)
### Services
Implements business logic and orchestration:
- Get-or-create patterns
- Data validation and transformation
- Complex workflows
[Documentation](lib/services/README.md)
### Extension
Arma 3 interface layer:
- Command routing and parsing
- Session management
- Error handling and logging
[Documentation](arma/server/extension/README.md)
### Client Mod
Client-side SQF addon that provides:
- **UI Components**: Player interfaces for inventory, organizations, banking
- **Event Handlers**: CBA event listeners for server communication
- **Optimistic Caching**: Local data caching for instant UI updates
- **State Management**: Client-side state synchronization
- **Input Validation**: Client-side validation before server requests
The client mod communicates with the server using **CBA Events**, ensuring:
- No direct extension calls from clients (security)
- Event-driven architecture for scalability
- Automatic state synchronization across all clients
- Reduced server load through client-side caching
## Available Commands
### Actor Commands
| Command | Description |
|---------|-------------|
| `actor:get` | Retrieve actor data by UID |
| `actor:create` | Create a new actor |
| `actor:update` | Update actor fields |
| `actor:exists` | Check if actor exists |
| `actor:delete` | Delete actor data |
### Organization Commands
| Command | Description |
|---------|-------------|
| `org:get` | Retrieve organization data |
| `org:create` | Create a new organization |
| `org:update` | Update organization fields |
| `org:exists` | Check if organization exists |
| `org:delete` | Delete organization |
| `org:add_member` | Add member to organization |
| `org:remove_member` | Remove member from organization |
| `org:get_members` | Get all organization members |
### Redis Operations
Direct Redis operations for advanced use cases:
- **Common**: Key-value operations (set, get, incr, decr, del)
- **Hash**: Structured data (hset, hget, hgetall, hdel)
- **List**: Ordered collections (lpush, rpush, lrange, lpop, rpop)
- **Set**: Unique collections (sadd, smembers, srem, sismember)
[Documentation](arma/server/extension/src/redis/README.md)
## Performance
- **Hot Cache (Server)**: < 1ms (HashMap lookup)
- **Cold Storage (Redis)**: 1-5ms (Network + Redis query)
- **Cache Hit Ratio**: ~95% for active players
- **Memory Usage**: ~1KB per active player (server), ~2KB per player (Redis)
## Contributing
We welcome contributions! Please see the contributing guides for each layer:
- [Extension Contributing Guide](arma/server/extension/README.md#contributing)
- [Services Contributing Guide](lib/services/README.md#contributing)
- [Repositories Contributing Guide](lib/repositories/README.md#contributing)
- [Models Contributing Guide](lib/models/README.md#contributing)
- [Library Contributing Guide](lib/README.md#contributing)
- [Adapter Contributing Guide](arma/server/extension/src/adapters/#contributing)
### Development Workflow
1. **Define Model**: Create data structure with validation
2. **Create Repository**: Implement persistence layer
3. **Build Service**: Add business logic
4. **Expose in Extension**: Create SQF-callable commands
5. **Test**: Verify each layer independently
## Error Handling
All commands return consistent error messages:
```sqf
private _result = "forge_server" callExtension ["actor:get", ["invalid_uid"]];
private _response = _result select 0;
if (_response find "Error:" == 0) then {
diag_log format ["Operation failed: %1", _response];
} else {
private _data = fromJSON _response;
// Use data
};
```
## Logging
Logs are automatically created in `@forge_server/logs/`:
- `actor.log` - Actor operations
- `org.log` - Organization operations
- `redis.log` - Redis connection and operations
- `debug.log` - General debug information
## License
[Your License Here]
## Support
- **Issues**: [Gitea Issues](https://gitea.innovativedevsolutions.org/IDSolutions/forge/issues)
- **Documentation**: See individual module READMEs
- **Architecture**: [FORGE_Architecture_Diagram.md](FORGE_Architecture_Diagram.md)
## Roadmap
- [ ] Admin system
- [ ] Arsenal system
- [ ] Banking system
- [ ] Economy system
- [ ] Garage system
- [ ] Locker system
- [ ] Mission template
---
Built using **Rust**, **Redis**, and **Arma 3**

12
arma/client/.editorconfig Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
end_of_line = crlf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.yml]
indent_size = 2

13
arma/client/.gitattributes vendored Normal file
View File

@ -0,0 +1,13 @@
# Sources
*.cpp text diff=cpp linguist-language=cpp
*.hpp text diff=cpp linguist-language=cpp
*.rhai text diff=rust linguist-language=rust
*.png binary
*.jpg binary
*.paa binary
# Linguistics
# Exclude included files and examples from stats
include/* linguist-vendored
extra/* linguist-vendored

12
arma/client/.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,12 @@
# Contributing Setup & Guidelines
## Setting up the Development Environment
### 1. Clone the repository from GitHub
### 2. Install HEMTT
The latest version of HEMTT can be installed by running:
```cmd
winget install hemtt
```
## Coding Guidelines
This mod follows the same coding guidelines as the ACE3 mod, which can be found [here](https://ace3.acemod.org/wiki/development/coding-guidelines).

View File

@ -0,0 +1,25 @@
---
name: Bug report
about: Create a bug report to help us improve
title: ''
labels: kind/bug
---
## Describe the bug
A clear and concise description of what the bug is.
## To reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
## Expected behavior
A clear and concise description of what you expected to happen.
## Attachments
If applicable, add screenshots or RPT logs to help explain your problem.
## Additional context
Add any other context about the problem here.

View File

@ -0,0 +1,15 @@
---
name: Feature Request
about: Suggest a feature to be added
title: ''
labels: kind/feature-request
---
## Describe the feature that you would like
A clear and concise description of the feature you'd want.
## Possible alternatives
Possible alternatives to your suggestion.
## Additional context
Add any other context about the feature here.

View File

@ -0,0 +1,12 @@
**When merged this pull request will:**
- Describe what this pull request will do
- Each change in a separate line
### Important
- [ ] If the contribution affects [the documentation](../docs), please include your changes in this pull request.
- [ ] [Development Guidelines](https://github.com/IDSolutions/MOD_REPO/blob/main/.github/CONTRIBUTING.md) are read, understood and applied.
- [ ] Title of this PR uses our standard template `Component - Add|Fix|Improve|Change|Make|Remove {changes}`.
<!-- Known issues that need to be addressed -->
### Known Issues
- [ ] Issue

View File

@ -0,0 +1 @@

28
arma/client/.github/workflows/check.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: HEMTT
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout the source code
uses: actions/checkout@v4
- name: Validate Config
run: python tools/config_style_checker.py
- name: Check for BOM
uses: arma-actions/bom-check@master
with:
path: "addons"
- name: Setup HEMTT
uses: arma-actions/hemtt@v1
- name: Run HEMTT check
run: hemtt check --pedantic

17
arma/client/.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
# HEMTT
hemtt.exe
.hemtt/missions/~*
.hemttout/
releases/
# Textures
Exports/
*.spp
*.spp.painter_lock
*.psd
# Other
*.biprivatekey
*.zip
*.pbo
*.sqfc

View File

@ -0,0 +1,28 @@
name: ctrlWebBrowserAction
description: Executes an action on a web browser control
groups:
- GUI Control
syntax:
- call: !Binary [control, actionArray]
ret:
- Nothing
- Nothing
params:
- name: control
type: Control
description: Web browser control to execute action on
- name: actionArray
type: ArrayUnknown
description: |
Array in format [actionType, actionData] where:
- actionType (String): Type of action ("ExecJS", "LoadURL", "Reload", "Stop", etc.)
- actionData (String): Data for the action (JavaScript code for ExecJS, URL for LoadURL, empty string for others)
argument_loc: Local
effect_loc: Local
since:
arma_3:
major: 2
minor: 2
examples:
- <sqf>_control ctrlWebBrowserAction ["ExecJS", "document.getElementById('test').innerHTML = 'Hello World!'"];</sqf>
- <sqf>_control ctrlWebBrowserAction ["LoadURL", "https://community.bistudio.com"];</sqf>

View File

@ -0,0 +1,13 @@
let readme = HEMTT_RFS.join("docs")
.join("README.md")
.open_file()
.read();
readme.replace("0.0.0",
HEMTT.project()
.version()
.to_string_short()
);
HEMTT_RFS.join("README.md")
.create_file()
.write(readme);
print("README.md version set to " + HEMTT.project().version());

View File

@ -0,0 +1,26 @@
let modcpp = HEMTT_VFS.join("mod.cpp")
.open_file()
.read();
modcpp.replace("0.0.0",
HEMTT.project()
.version()
.to_string_short()
);
HEMTT_VFS.join("mod.cpp")
.create_file()
.write(modcpp);
print("mod.cpp version set to " + HEMTT.project().version());
// Currently unused, but included anyway
let readme = HEMTT_VFS.join("README.md")
.open_file()
.read();
readme.replace("0.0.0",
HEMTT.project()
.version()
.to_string_short()
);
HEMTT_VFS.join("README.md")
.create_file()
.write(readme);
print("README.md version set to " + HEMTT.project().version());

View File

@ -0,0 +1,16 @@
[default]
workshop = [
"450814997", # CBA_A3
"3499977893", # Advanced Dev Tools
"623475643", # 3DEN Enhanced
]
presets = []
dlc = []
optionals = []
parameters = []
[ace]
extends = "default"
workshop = [
"463939057", # ACE
]

View File

@ -0,0 +1,40 @@
[sqf.banned_commands]
options.banned = [
# "spawn", # Scheduled should be avoided whenever possible
"execVM", # Script files should never be run directly, they should be functions
"remoteExec", # CBA events should be used for networking
]
[sqf.banned_macros]
options.release = [
"DEBUG_MODE_FULL",
"DISABLE_COMPILE_CACHE",
]
[sqf.event_unknown]
options.ignore = [
"JSDialog",
]
[sqf.this_call]
enabled = true
[sqf.undefined]
enabled = true
options.check_orphan_code = true
[sqf.unused]
enabled = true # many false positives without DEBUG_MODE_FULL
options.check_params = false
[sqf.shadowed]
enabled = false
[sqf.not_private]
enabled = true
[config.file_type]
options.allow_no_extension = false
[stringtables.usage]
options.ignore_unused = true

View File

@ -0,0 +1,24 @@
name = "forge-client"
author = "J.Schmidt"
prefix = "forge_client"
mainprefix = "forge"
[version]
path = "addons/main/script_version.hpp"
git_hash = 0
[files]
include = [
"mod.cpp",
"meta.cpp",
"logo_forge_client.png",
"logo_forge_client_over.png",
"logo_forge_client_ca.paa",
"logo_forge_client_over_ca.paa",
"LICENSE.md",
"README.md",
]
exclude = []
[properties]
author = "J.Schmidt"

View File

@ -0,0 +1,19 @@
// Read the current contents of script_version.hpp
let script_version = HEMTT_RFS.join("addons")
.join("main")
.join("script_version.hpp")
.open_file()
.read();
// Replace the current version with the new version
let prefix = "#define BUILD ";
let current = HEMTT.project().version().build();
let next = current + 1;
script_version.replace(prefix + current.to_string(), prefix + next.to_string());
// Write the modified contents to script_version.hpp
HEMTT_RFS.join("addons")
.join("main")
.join("script_version.hpp")
.create_file()
.write(script_version);

View File

@ -0,0 +1,23 @@
// Read the current contents of script_version.hpp
let script_version = HEMTT_RFS.join("addons")
.join("main")
.join("script_version.hpp")
.open_file()
.read();
// Replace the current version with the new version
let prefix = "#define MINOR ";
let current = HEMTT.project().version().minor();
let next = current + 1;
// Updating minor version should reset patch number
script_version.replace(prefix + current.to_string(), prefix + next.to_string());
current = HEMTT.project().version().patch();
script_version.replace("#define PATCH " + current.to_string(), "#define PATCH 0");
// Write the modified contents to script_version.hpp
HEMTT_RFS.join("addons")
.join("main")
.join("script_version.hpp")
.create_file()
.write(script_version);

View File

@ -0,0 +1,20 @@
// Read the current contents of script_version.hpp
let script_version = HEMTT_RFS.join("addons")
.join("main")
.join("script_version.hpp")
.open_file()
.read();
// Replace the current version with the new version
let prefix = "#define PATCH ";
let current = HEMTT.project().version().patch();
let next = current + 1;
script_version.replace(prefix + current.to_string(), prefix + next.to_string());
// Write the modified contents to script_version.hpp
HEMTT_RFS.join("addons")
.join("main")
.join("script_version.hpp")
.create_file()
.write(script_version);

119
arma/client/LICENSE.md Normal file
View File

@ -0,0 +1,119 @@
![APL-SA](https://www.bohemia.net/assets/img/licenses/APL-SA.png)
## Brief summary of this Licence
PLEASE, NOTE THAT THIS SUMMARY HAS NO LEGAL EFFECT AND IS ONLY OF AN INFORMATORY NATURE DESIGNED FOR YOU TO GET THE BASIC INFORMATION ABOUT THE CONTENT OF THIS LICENCE. THE ONLY LEGALLY BINDING PROVISIONS ARE THOSE IN THE ORIGINAL AND FULL TEXT OF THIS LICENCE.
With this licence you are free to adapt (i.e. modify, rework or update) and share (i.e. copy, distribute or transmit) the material under the following conditions:
* **Attribution** - You must attribute the material in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the material).
* **Noncommercial** - You may not use this material for any commercial purposes.
* **Arma Only** - You may not convert or adapt this material to be used in other games than Arma.
* **Share Alike** - If you adapt, or build upon this material, you may distribute the resulting material only under the same license.
---
# Full version of licence
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Arma Public License - Share Alike ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
### Section 1 Definitions
1. **Adapted Material** means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
2. **Adapter's License** means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
3. **ArmaOnly** means primarily intended for or directed towards the use in any of existing and future Arma games, including but not limited to Arma: Cold War Assault, Arma, Arma 2 and Arma 3 and its official sequels and expansion packs.
4. **Arma Public Share Alike Compatible License** means a license listed at [https://www.bohemia.net/community/licenses](https://www.bohemia.net/community/licenses) as essentially the equivalent of this Public License.
5. **Copyright and Similar Rights** means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
6. **Effective Technological Measures** means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
7. **Exceptions and Limitations** means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
8. **Licensed Material** means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
9. **Licensed Rights** means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
10. **Licensor** means the individual(s) or entity(ies) granting rights under this Public License.
11. **NonCommercial** means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange.
12. **Share** means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
13. **Sui Generis Database Rights** means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
14. **You** means the individual or entity exercising the Licensed Rights under this Public License. **Your** has a corresponding meaning.
### Section 2 Scope
1. **License grant**
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
1. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial and ArmaOnly purposes only; and
2. produce, reproduce, and Share Adapted Material for NonCommercial and ArmaOnly purposes only.
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
3. Term. The term of this Public License is specified in Section 6(a).
4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
5. Downstream recipients.
1. Offer from the Licensor Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
2. Additional offer from the Licensor Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapters License You apply.
3. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(a)(i).
2. **Other rights**
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this Public License.
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial and ArmaOnly purposes.
### Section 3 License Conditions
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
1. **Attribution**
1. If You Share the Licensed Material (including in modified form), You must:
1. retain the following if it is supplied by the Licensor with the Licensed Material:
1. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
2. a copyright notice;
3. a notice that refers to this Public License;
4. a notice that refers to the disclaimer of warranties;
5. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
2. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
3. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(a) to the extent reasonably practicable.
2. **ShareAlike**
In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
1. The Adapters License You apply must be this Public License, or an Arma Public Share Alike Compatible License.
2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
### Section 4 Sui Generis Database Rights
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
1. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial and ArmaOnly purposes only;
2. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
3. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
### Section 5 Disclaimer of Warranties and Limitation of Liability
1. **Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.**
2. **To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.**
3. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
### Section 6 Term and Termination
1. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
2. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
3. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
4. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
### Section 7 Other Terms and Conditions
1. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
2. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
### Section 8 Interpretation
1. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
2. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
3. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
4. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
### Bohemia Interactive Notices
1. Bohemia Interactive a.s. is not a party to this License, and makes no warranty whatsoever in connection with the Licensed Material. Bohemia Interactive a.s. will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, Bohemia Interactive a.s. may elect to apply the Public License to material it publishes and in those instances it becomes the "Licensor".
2. Except for the limited purpose of indicating to the public that the Licensed Material is shared under this Public License, Bohemia Interactive a.s. does not authorize the use by either party of the trademarks "Arma", "Bohemia Interactive" or any related trademark or logo of Arma or Bohemia Interactive without the prior written consent of Bohemia Interactive a.s.

27
arma/client/README.md Normal file
View File

@ -0,0 +1,27 @@
<h1 align="center">Forge Client</h1>
<p align="center">
<a href="https://gitea.innovativedevsolutions.org/IDSolutions/forge/releases/latest"><img src="https://img.shields.io/gitea/v/release/IDSolutions/forge?gitea_url=https%3A%2F%2Fgitea.innovativedevsolutions.org&label=Version" alt="Version"></a>
<a href="https://gitea.innovativedevsolutions.org/IDSolutions/forge/issues"><img src="https://img.shields.io/gitea/issues/open/IDSolutions/forge?gitea_url=https%3A%2F%2Fgitea.innovativedevsolutions.org&label=Issues" alt="Issues"></a>
<!-- <a href="https://steamcommunity.com/sharedfiles/filedetails/?id=MOD_ID"><img src="https://img.shields.io/steam/downloads/MOD_ID.svg?&label=Downloads" alt="Downloads"></a> -->
<a href="https://gitea.innovativedevsolutions.org/IDSolutions/forge/src/branch/master/arma/server/LICENSE.md"><img src="https://img.shields.io/badge/License-APL%20SA-red?label=License" alt="License"></a>
<br>
<img src="https://img.shields.io/github/v/release/brettmayson/hemtt?label=HEMTT" alt="HEMTT">
<img src="https://img.shields.io/github/v/release/cbateam/cba_a3?label=CBA%20A3" alt="CBA A3">
</p>
<p align="center">
<b>Requires the latest version of <a href="https://github.com/CBATeam/CBA_A3/releases/latest">CBA A3</a></b>
</p>
**Forge Client** aims to...
The project is entirely **open-source** and any contributions are welcome.
## Core Features
- Feature
## Contributing
For new contributers, see the [Contributing Setup & Guidelines](./.github/CONTRIBUTING.md).
## License
Forge Client is licensed under [APL-SA](./LICENSE.md).

View File

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

View File

@ -0,0 +1,19 @@
class Extended_PreStart_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
};
};
class Extended_PreInit_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_preInitClient));
};
};
class Extended_PostInit_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_postInit));
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_postInitClient));
};
};

View File

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

View File

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

View File

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

View File

@ -0,0 +1,42 @@
#include "script_component.hpp"
removeAllWeapons player;
removeAllAssignedItems player;
removeUniform player;
removeVest player;
removeBackpack player;
removeGoggles player;
removeHeadgear player;
SETPVAR(player,FORGE_actorIsLoaded,false);
cutText ["Loading In...", "BLACK", 1];
if (isNil QGVAR(ActorClass)) then { [] call FUNC(initActorClass); };
[QGVAR(initActor), {
GVAR(ActorClass) call ["init", []];
}] call CFUNC(addEventHandler);
[QGVAR(responseInitActor), {
params [["_data", createHashMap, [createHashMap]]];
GVAR(ActorClass) call ["sync", [_data, true]];
SETPVAR(player,FORGE_isLoaded,true);
cutText ["", "PLAIN", 1];
}] call CFUNC(addEventHandler);
[QGVAR(responseSyncActor), {
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
GVAR(ActorClass) call ["sync", [_data, _jip]];
}] call CFUNC(addEventHandler);
[QGVAR(initActor), []] call CFUNC(localEvent);
[{
GETVAR(player,FORGE_actorIsLoaded,false)
}, {
private _holster = GVAR(ActorClass) call ["get", ["holster", true]];
if (_holster) then { [player] call AFUNC(weaponselect,putWeaponAway); };
}] call CFUNC(waitUntilAndExecute);

View File

@ -0,0 +1,10 @@
#include "script_component.hpp"
PREP_RECOMPILE_START;
#include "XEH_PREP.hpp"
PREP_RECOMPILE_END;
private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
#include "initSettings.inc.sqf"
#include "initKeybinds.inc.sqf"

View File

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

View File

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

View File

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

View File

@ -0,0 +1,45 @@
#include "..\script_component.hpp"
/*
* Author: IDSolutions
* Handles the UI events.
*
* Arguments:
* None
*
* Return Value:
* None
*
* Example:
* [] call forge_client_actor_fnc_handleUIEvents;
*
* Public: No
*/
params ["_control", "_isConfirmDialog", "_message"];
private _alert = fromJSON _message;
private _event = _alert get "event";
private _data = _alert get "data";
private _display = displayChild findDisplay 46;
diag_log format ["[FORGE:Client:Actor] Handling UI event: %1 with data: %2", _event, _data];
switch (_event) do {
case "actor::get::actions": { GVAR(ActorClass) call ["getNearbyActions", [_control]]; };
// case "actor::open::bank": { [] spawn EFUNC(bank,openUI); };
case "actor::open::bank": { [] spawn EFUNC(bank,openUI); };
case "actor::open::device": { hint "Device interaction is not yet implemented."; }; // TODO: Implement device interaction
case "actor::open::garage": { hint "Garage interaction is not yet implemented."; }; // TODO: Implement garage interaction
case "actor::open::org": { [] spawn EFUNC(org,openUI); };
case "actor::open::locker": { hint "Locker interaction is not yet implemented."; }; // TODO: Implement locker interaction
// case "actor::open::phone": { [] spawn EFUNC(phone,openUI) };
case "actor::open::phone": { hint "Phone interaction is not yet implemented."; }; // TODO: Implement phone interaction
case "actor::open::iplayer": { hint "Player interaction is not yet implemented." }; // TODO: Implement player interaction
case "actor::open::store": { hint "Store interaction is not yet implemented."; }; // TODO: Implement store interaction
default { hint format ["Unhandled UI event: %1", _event]; };
};
if (_event isNotEqualTo "actor::get::actions") then { _display closeDisplay 1; };
true;

View File

@ -0,0 +1,165 @@
#include "..\script_component.hpp"
/*
* Author: IDSolutions
* Initializes the actor class.
*
* Arguments:
* None
*
* Return Value:
* None
*
* Examples:
* [] call forge_client_actor_fnc_initActorClass
*
* Public: Yes
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(ActorClass) = createHashMapObject [[
["#type", "IActorClass"],
["#create", {
_self set ["uid", getPlayerUID player];
_self set ["actor", createHashMap];
_self set ["isLoaded", false];
_self set ["lastSave", time];
private _actor = createHashMap;
_actor set ["uid", (getPlayerUID player)];
_actor set ["name", (name player)];
_actor set ["loadout", [[],[],[],["U_BG_Guerrilla_6_1",[]],[],[],"H_Cap_blk_ION","",[],["ItemMap","ItemGPS","ItemRadio","ItemCompass","ItemWatch",""]]];
_actor set ["position", (getPosASL player)];
_actor set ["direction", (getDir player)];
_actor set ["stance", (stance player)];
_actor set ["rank", (rank player)];
_actor set ["state", (lifeState player)];
_actor set ["phone_number", ""];
_actor set ["email", ""];
_actor set ["organization", ""];
_actor set ["holster", true];
_self set ["actor", _actor];
}],
["init", {
private _uid = _self get "uid";
private _actor = _self get "actor";
[SRPC(actor,requestInitActor), [_uid, _actor]] call CFUNC(serverEvent);
systemChat format ["Actor loaded for %1", (name player)];
diag_log "[FORGE:Client:Actor] Actor Class Initialized!";
}],
["save", {
params [["_sync", false, [false]]];
private _uid = _self get "uid";
[SRPC(actor,requestSaveActor), [_uid, _sync]] call CFUNC(serverEvent);
_self set ["lastSave", time];
}],
["sync", {
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
private _actor = _self get "actor";
private _isLoaded = _self get "isLoaded";
if !(_isLoaded) then { _self set ["isLoaded", true]; };
if (_data isEqualTo createHashMap) exitWith {
diag_log "[FORGE:Client:Actor] Empty data received for sync, skipping.";
};
{
_actor set [_x, _y];
if (_jip) then {
switch (_x) do {
case "position": { _self call ["applyPosition"]; };
case "direction": { _self call ["applyDirection"]; };
case "stance": { _self call ["applyStance"]; };
case "rank": { _self call ["applyRank"]; };
case "loadout": { _self call ["applyLoadout"]; };
default {};
};
};
} forEach _data;
_self set ["actor", _actor];
SETPVAR(player,FORGE_actorIsLoaded,true);
diag_log "[FORGE:Client:Actor] Sync completed";
}],
["get", {
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
private _actor = _self get "actor";
_actor getOrDefault [_key, _default];
}],
["applyPosition", {
private _position = _self call ["get", ["position", [0, 0, 0]]];
if (GVAR(enableLoc)) then {
player setPosASL _position;
private _pAlt = ((getPosATLVisual player) select 2);
private _pVelZ = ((velocity player) select 2);
if (_pAlt > 5 && _pVelZ < 0) then {
player setVelocity [0, 0, 0];
player setPosATL [((getPosATLVisual player) select 0), ((getPosATLVisual player) select 1), 1];
hint "You logged off mid air. You were moved to a safe position on the ground";
};
};
}],
["applyDirection", {
private _direction = _self call ["get", ["direction", 0]];
if (GVAR(enableLoc)) then { player setDir _direction; };
}],
["applyStance", {
private _stance = _self call ["get", ["stance", "STAND"]];
if (GVAR(enableLoc)) then { player playAction _stance; };
}],
["applyRank", {
private _rank = _self call ["get", ["rank", "PRIVATE"]];
player setUnitRank _rank;
}],
["applyLoadout", {
private _loadout = _self call ["get", ["loadout", []]];
if (GVAR(enableGear) && count _loadout > 0) then { player setUnitLoadout _loadout; };
}],
["getNearbyActions", {
params [["_control", controlNull, [controlNull]]];
private _nearbyActions = [];
{
private _storeType = _x getVariable ["storeType", ""];
private _isBank = _x getVariable ["isBank", false];
private _isGarage = _x getVariable ["isGarage", false];
private _isLocker = _x getVariable ["isLocker", false];
private _garageType = _x getVariable ["garageType", ""];
private _deviceType = _x getVariable ["deviceType", ""];
private _isPlayer = _x isKindOf "Man" && isPlayer _x;
if (_storeType isNotEqualTo "") then { _nearbyActions pushBack ["store", _storeType]; };
if (_isBank) then { _nearbyActions pushBack ["bank", true]; };
if (_isLocker) then { _nearbyActions pushBack ["locker", true]; };
if (_isLocker && GVAR(enableVA)) then { _nearbyActions pushBack ["va", true]; };
if (_isGarage) then { _nearbyActions pushBack ["garage", _garageType]; };
if (_isGarage && GVAR(enableVG)) then { _nearbyActions pushBack ["vg", true]; };
if (_deviceType isNotEqualTo "") then { _nearbyActions pushBack ["device", _deviceType]; };
if (_isPlayer) then { _nearbyActions pushBack ["player", name _x]; };
} forEach (player nearObjects 5);
_control ctrlWebBrowserAction ["ExecJS", format ["updateAvailableActions(%1)", (toJSON _nearbyActions)]];
}]
]];
SETVAR(player,FORGE_ActorClass,GVAR(ActorClass));
GVAR(ActorClass)

View File

@ -0,0 +1,31 @@
#include "..\script_component.hpp"
/*
* Author: IDSolutions
* Opens the player interaction interface.
*
* Arguments:
* None
*
* Return Value:
* None
*
* Example:
* [] call forge_client_actor_fnc_openUI;
*
* Public: No
*/
private _display = (findDisplay 46) createDisplay "RscActorMenu";
private _ctrl = (_display displayCtrl 1001);
_ctrl ctrlAddEventHandler ["JSDialog", {
params ["_control", "_isConfirmDialog", "_message"];
[_control, _isConfirmDialog, _message] call FUNC(handleUIEvents);
}];
_ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\index.html)];
// _ctrl ctrlWebBrowserAction ["OpenDevConsole"];
true;

View File

@ -0,0 +1,8 @@
#include "\forge\forge_client\addons\main\data\hpp\defineDIKCodes.hpp"
[
_category, QGVAR(ForgeIMenu),
[LSTRING(iMenu), LSTRING(iMenuTooltip)], {
call FUNC(openUI)
}, {}, [DIK_TAB, false, false, false] // Default keybind
] call CBA_fnc_addKeybind;

View File

@ -0,0 +1,24 @@
// Can use localize "STR_ACE_Common_Enabled" for name if ACE is required
[
QGVAR(enableLoc), "CHECKBOX",
[LSTRING(enableLoc), LSTRING(enableLocTooltip)],
_category, true, true
] call CBA_fnc_addSetting;
[
QGVAR(enableGear), "CHECKBOX",
[LSTRING(enableGear), LSTRING(enableGearTooltip)],
_category, true, true
] call CBA_fnc_addSetting;
[
QGVAR(enableVA), "CHECKBOX",
[LSTRING(enableVA), LSTRING(enableVATooltip)],
_category, false, true
] call CBA_fnc_addSetting;
[
QGVAR(enableVG), "CHECKBOX",
[LSTRING(enableVG), LSTRING(enableVGTooltip)],
_category, false, true
] call CBA_fnc_addSetting;

View File

@ -0,0 +1,9 @@
#define COMPONENT actor
#define COMPONENT_BEAUTIFIED Actor
#include "\forge\forge_client\addons\main\script_mod.hpp"
// #define DEBUG_MODE_FULL
// #define DISABLE_COMPILE_CACHE
// #define ENABLE_PERFORMANCE_COUNTERS
#include "\forge\forge_client\addons\main\script_macros.hpp"

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<Project name="FFE">
<Package name="Actor">
<Key ID="STR_forge_client_actor_displayName">
<English>Actor</English>
</Key>
<Key ID="STR_forge_client_actor_enableGear">
<English>Persistent Gear</English>
</Key>
<Key ID="STR_forge_client_actor_enableGearTooltip">
<English>Enable Persistent Gear</English>
</Key>
<Key ID="STR_forge_client_actor_enableLoc">
<English>Persistent Location</English>
</Key>
<Key ID="STR_forge_client_actor_enableLocTooltip">
<English>Enable Persistent Location</English>
</Key>
<Key ID="STR_forge_client_actor_enableVA">
<English>Virtual Arsenal</English>
</Key>
<Key ID="STR_forge_client_actor_enableVATooltip">
<English>Enable Virtual Arsenal</English>
</Key>
<Key ID="STR_forge_client_actor_enableVG">
<English>Virtual Garage</English>
</Key>
<Key ID="STR_forge_client_actor_enableVGTooltip">
<English>Enable Virtual Garage</English>
</Key>
<Key ID="STR_forge_client_actor_iMenu">
<English>Interaction Menu</English>
</Key>
<Key ID="STR_forge_client_actor_iMenuTooltip">
<English>Open your interaction menu</English>
</Key>
</Package>
</Project>

View File

@ -0,0 +1,21 @@
class RscActorMenu {
idd = 1000;
fadeIn = 0;
fadeOut = 0;
duration = 1e011;
onLoad = "uiNamespace setVariable ['RscActorMenu', _this select 0]";
onUnLoad = "uinamespace setVariable ['RscActorMenu', nil]";
class controlsBackground {};
class controls {
class IFrame: RscText {
type = 106;
idc = 1001;
x = "safeZoneXAbs";
y = "safeZoneY";
w = "safeZoneWAbs";
h = "safeZoneH";
colorBackground[] = {0, 0, 0, 0};
};
};
};

View File

@ -0,0 +1,98 @@
// Control types
#define CT_STATIC 0
#define CT_BUTTON 1
#define CT_EDIT 2
#define CT_SLIDER 3
#define CT_COMBO 4
#define CT_LISTBOX 5
#define CT_TOOLBOX 6
#define CT_CHECKBOXES 7
#define CT_PROGRESS 8
#define CT_HTML 9
#define CT_STATIC_SKEW 10
#define CT_ACTIVETEXT 11
#define CT_TREE 12
#define CT_STRUCTURED_TEXT 13
#define CT_CONTEXT_MENU 14
#define CT_CONTROLS_GROUP 15
#define CT_SHORTCUTBUTTON 16
#define CT_HITZONES 17
#define CT_XKEYDESC 40
#define CT_XBUTTON 41
#define CT_XLISTBOX 42
#define CT_XSLIDER 43
#define CT_XCOMBO 44
#define CT_ANIMATED_TEXTURE 45
#define CT_OBJECT 80
#define CT_OBJECT_ZOOM 81
#define CT_OBJECT_CONTAINER 82
#define CT_OBJECT_CONT_ANIM 83
#define CT_LINEBREAK 98
#define CT_USER 99
#define CT_MAP 100
#define CT_MAP_MAIN 101
#define CT_LISTNBOX 102
#define CT_ITEMSLOT 103
#define CT_CHECKBOX 77
// Static styles
#define ST_POS 0x0F
#define ST_HPOS 0x03
#define ST_VPOS 0x0C
#define ST_LEFT 0x00
#define ST_RIGHT 0x01
#define ST_CENTER 0x02
#define ST_DOWN 0x04
#define ST_UP 0x08
#define ST_VCENTER 0x0C
#define ST_TYPE 0xF0
#define ST_SINGLE 0x00
#define ST_MULTI 0x10
#define ST_TITLE_BAR 0x20
#define ST_PICTURE 0x30
#define ST_FRAME 0x40
#define ST_BACKGROUND 0x50
#define ST_GROUP_BOX 0x60
#define ST_GROUP_BOX2 0x70
#define ST_HUD_BACKGROUND 0x80
#define ST_TILE_PICTURE 0x90
#define ST_WITH_RECT 0xA0
#define ST_LINE 0xB0
#define ST_UPPERCASE 0xC0
#define ST_LOWERCASE 0xD0
#define ST_SHADOW 0x100
#define ST_NO_RECT 0x200
#define ST_KEEP_ASPECT_RATIO 0x800
// Slider styles
#define SL_DIR 0x400
#define SL_VERT 0
#define SL_HORZ 0x400
#define SL_TEXTURES 0x10
// progress bar
#define ST_VERTICAL 0x01
#define ST_HORIZONTAL 0
// Listbox styles
#define LB_TEXTURES 0x10
#define LB_MULTI 0x20
// Tree styles
#define TR_SHOWROOT 1
#define TR_AUTOCOLLAPSE 2
// Default text sizes
#define GUI_TEXT_SIZE_SMALL (GUI_GRID_H * 0.8)
#define GUI_TEXT_SIZE_MEDIUM (GUI_GRID_H * 1)
#define GUI_TEXT_SIZE_LARGE (GUI_GRID_H * 1.2)
// Pixel grid
#define pixelScale 0.50
#define GRID_W (pixelW * pixelGrid * pixelScale)
#define GRID_H (pixelH * pixelGrid * pixelScale)
class RscText;

View File

@ -0,0 +1,605 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 100vh;
width: 100vw;
background: rgba(0, 0, 0, 0.7);
font-family: Arial, sans-serif;
color: rgba(200, 220, 240, 0.95);
overflow: hidden;
}
.garage-container {
height: 100vh;
width: 100vw;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Header Section */
.garage-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);
}
.garage-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;
}
.garage-info {
flex: 1;
}
.garage-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;
}
.garage-subtitle {
font-size: 0.875rem;
color: rgba(140, 160, 180, 0.8);
letter-spacing: 0.5px;
}
.garage-stats {
display: flex;
gap: 1.5rem;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 0.75rem 1.25rem;
background: rgba(20, 30, 45, 0.7);
border: 1px solid rgba(100, 150, 200, 0.3);
border-radius: 4px;
}
.stat-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(140, 160, 180, 0.8);
}
.stat-value {
font-size: 1.25rem;
font-weight: 600;
color: rgba(100, 200, 150, 1);
}
.header-actions {
display: flex;
gap: 0.75rem;
}
.action-btn {
padding: 0.625rem 1.25rem;
background: rgba(20, 30, 45, 0.7);
border: 1px solid rgba(100, 150, 200, 0.4);
border-radius: 4px;
color: rgba(200, 220, 240, 0.95);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
transition: all 0.15s ease;
}
.action-btn:hover {
background: rgba(30, 45, 70, 0.9);
border-color: rgba(150, 200, 255, 0.7);
box-shadow:
0 0 15px rgba(100, 150, 200, 0.2),
inset 0 0 20px rgba(100, 150, 200, 0.05);
}
.close-btn {
border-color: rgba(200, 100, 100, 0.4);
}
.close-btn:hover {
border-color: rgba(255, 100, 100, 0.7);
box-shadow:
0 0 15px rgba(200, 100, 100, 0.2),
inset 0 0 20px rgba(200, 100, 100, 0.05);
}
/* Main Content */
.garage-content {
flex: 1;
display: grid;
grid-template-columns: 250px 1fr 350px;
gap: 1.5rem;
overflow: hidden;
}
/* Panels */
.garage-panel {
background: rgba(15, 20, 30, 0.9);
border: 1px solid rgba(100, 150, 200, 0.4);
border-left: 3px solid rgba(100, 150, 200, 0.5);
border-radius: 4px;
display: flex;
flex-direction: column;
box-shadow:
0 0 20px rgba(100, 150, 200, 0.1),
0 4px 16px rgba(0, 0, 0, 0.6);
}
.panel-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid rgba(100, 150, 200, 0.2);
}
.panel-title {
font-size: 1rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(200, 220, 255, 1);
}
.panel-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
}
/* Custom Scrollbar */
.panel-content::-webkit-scrollbar {
width: 8px;
}
.panel-content::-webkit-scrollbar-track {
background: rgba(15, 20, 30, 0.5);
border-radius: 4px;
}
.panel-content::-webkit-scrollbar-thumb {
background: rgba(100, 150, 200, 0.3);
border-radius: 4px;
}
.panel-content::-webkit-scrollbar-thumb:hover {
background: rgba(100, 150, 200, 0.5);
}
/* Filters */
.filter-section {
margin-bottom: 2rem;
}
.filter-section:last-child {
margin-bottom: 0;
}
.filter-title {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(160, 180, 200, 0.85);
margin-bottom: 0.75rem;
}
.filter-buttons {
display: flex;
gap: 0.5rem;
}
.filter-btn {
flex: 1;
padding: 0.625rem;
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.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
transition: all 0.15s ease;
}
.filter-btn:hover {
background: rgba(30, 45, 70, 0.8);
border-color: rgba(150, 200, 255, 0.5);
}
.filter-btn.active {
background: rgba(100, 150, 200, 0.2);
border-color: rgba(100, 150, 200, 0.6);
box-shadow:
0 0 10px rgba(100, 150, 200, 0.15),
inset 0 0 15px rgba(100, 150, 200, 0.05);
}
.type-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.type-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.875rem;
background: rgba(20, 30, 45, 0.7);
border: 1px solid rgba(100, 150, 200, 0.3);
border-left: 3px solid rgba(100, 150, 200, 0.4);
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
text-align: left;
}
.type-item:hover {
background: rgba(30, 45, 70, 0.8);
border-left-color: rgba(150, 200, 255, 0.7);
}
.type-item.active {
background: rgba(30, 45, 70, 0.9);
border-left-color: rgba(100, 200, 150, 0.8);
box-shadow:
0 0 15px rgba(100, 200, 150, 0.15),
inset 0 0 20px rgba(100, 200, 150, 0.05);
}
.type-icon {
font-size: 1.5rem;
}
.type-name {
font-size: 0.875rem;
color: rgba(200, 220, 240, 0.95);
}
.search-input {
width: 100%;
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;
}
.search-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);
}
.search-input::placeholder {
color: rgba(100, 120, 140, 0.6);
}
/* Vehicles Grid */
.vehicles-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.vehicle-card {
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;
padding: 1.25rem;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.vehicle-card:hover {
background: rgba(30, 45, 70, 0.7);
border-left-color: rgba(150, 200, 255, 0.7);
box-shadow:
0 0 15px rgba(100, 150, 200, 0.15),
inset 0 0 20px rgba(100, 150, 200, 0.05);
}
.vehicle-card.selected {
background: rgba(30, 45, 70, 0.8);
border-left-color: rgba(100, 200, 150, 0.8);
box-shadow:
0 0 20px rgba(100, 200, 150, 0.2),
inset 0 0 25px rgba(100, 200, 150, 0.05);
}
.vehicle-icon {
font-size: 3rem;
text-align: center;
}
.vehicle-name {
font-size: 0.95rem;
font-weight: 600;
color: rgba(200, 220, 255, 1);
text-align: center;
}
.vehicle-type {
font-size: 0.75rem;
color: rgba(140, 160, 180, 0.85);
text-align: center;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.vehicle-status {
padding: 0.375rem;
background: rgba(100, 150, 200, 0.2);
border: 1px solid rgba(100, 150, 200, 0.3);
border-radius: 3px;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.5px;
text-align: center;
font-weight: 600;
}
.vehicle-status.stored {
background: rgba(100, 150, 200, 0.2);
border-color: rgba(100, 150, 200, 0.4);
color: rgba(150, 200, 255, 0.9);
}
.vehicle-status.active {
background: rgba(100, 200, 150, 0.2);
border-color: rgba(100, 200, 150, 0.4);
color: rgba(150, 255, 200, 0.9);
}
/* Vehicle Details */
.no-selection {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 3rem 1rem;
}
.no-selection-icon {
font-size: 4rem;
opacity: 0.3;
}
.no-selection p {
font-size: 0.875rem;
color: rgba(140, 160, 180, 0.7);
}
.vehicle-details {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.detail-header {
display: flex;
align-items: center;
gap: 1rem;
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;
}
.detail-icon {
font-size: 3rem;
}
.detail-info {
flex: 1;
}
.detail-name {
font-size: 1.125rem;
font-weight: 600;
color: rgba(200, 220, 255, 1);
margin-bottom: 0.25rem;
}
.detail-type {
font-size: 0.75rem;
color: rgba(140, 160, 180, 0.85);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.detail-stat {
padding: 0.875rem;
background: rgba(20, 30, 45, 0.6);
border: 1px solid rgba(100, 150, 200, 0.3);
border-radius: 4px;
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.detail-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(140, 160, 180, 0.85);
}
.detail-value {
font-size: 0.95rem;
font-weight: 600;
color: rgba(200, 220, 240, 0.95);
}
.detail-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.detail-btn {
padding: 1rem;
background: rgba(20, 30, 45, 0.7);
border: 1px solid rgba(100, 150, 200, 0.4);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
cursor: pointer;
transition: all 0.15s ease;
}
.detail-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);
}
.spawn-btn {
background: rgba(100, 150, 200, 0.2);
border-color: rgba(100, 150, 200, 0.5);
}
.spawn-btn:hover {
background: rgba(100, 150, 200, 0.3);
border-color: rgba(150, 200, 255, 0.7);
}
.store-btn {
background: rgba(200, 150, 100, 0.2);
border-color: rgba(200, 150, 100, 0.4);
}
.store-btn:hover {
background: rgba(200, 150, 100, 0.3);
border-color: rgba(255, 200, 150, 0.6);
}
.btn-icon {
font-size: 1.25rem;
}
.btn-text {
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(200, 220, 240, 0.95);
}
.detail-specs {
padding: 1.25rem;
background: rgba(20, 30, 45, 0.5);
border: 1px solid rgba(100, 150, 200, 0.2);
border-radius: 4px;
}
.specs-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;
}
.specs-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.spec-item {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(100, 150, 200, 0.15);
}
.spec-item:last-child {
padding-bottom: 0;
border-bottom: none;
}
.spec-label {
font-size: 0.8rem;
color: rgba(140, 160, 180, 0.85);
}
.spec-value {
font-size: 0.875rem;
font-weight: 600;
color: rgba(200, 220, 240, 0.95);
}
/* Responsive */
@media (max-width: 1400px) {
.garage-content {
grid-template-columns: 220px 1fr 320px;
}
.vehicles-grid {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}
}
@media (max-width: 1200px) {
.garage-content {
grid-template-columns: 1fr 350px;
}
.filters-panel {
display: none;
}
}

View File

@ -0,0 +1,205 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vehicle Garage</title>
<link rel="stylesheet" href="garage.css" />
<!--
Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API
This approach is used instead of static HTML imports to work with Arma 3's file system
-->
<script>
Promise.all([
A3API.RequestFile(
"forge\\forge_client\\addons\\actor\\ui\\_site\\garage.css",
),
A3API.RequestFile(
"forge\\forge_client\\addons\\actor\\ui\\_site\\garage.js",
),
]).then(([css, js]) => {
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
const script = document.createElement("script");
script.text = js;
document.head.appendChild(script);
});
</script>
</head>
<body>
<div class="garage-container">
<!-- Header Section -->
<div class="garage-header">
<div class="garage-logo">
<div class="logo-icon">🚗</div>
</div>
<div class="garage-info">
<h1 class="garage-title">Vehicle Garage</h1>
<p class="garage-subtitle">Vehicle Management System</p>
</div>
<div class="garage-stats">
<div class="stat-item">
<span class="stat-label">Stored</span>
<span class="stat-value" id="storedCount">12</span>
</div>
<div class="stat-item">
<span class="stat-label">Active</span>
<span class="stat-value" id="activeCount">2</span>
</div>
<div class="stat-item">
<span class="stat-label">Capacity</span>
<span class="stat-value" id="capacityCount">20</span>
</div>
</div>
<div class="header-actions">
<button class="action-btn close-btn">Close</button>
</div>
</div>
<!-- Main Content -->
<div class="garage-content">
<!-- Left Panel - Filters -->
<div class="garage-panel filters-panel">
<div class="panel-header">
<h2 class="panel-title">Filters</h2>
</div>
<div class="panel-content">
<!-- Status Filter -->
<div class="filter-section">
<h3 class="filter-title">Status</h3>
<div class="filter-buttons">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="stored">Stored</button>
<button class="filter-btn" data-filter="active">Active</button>
</div>
</div>
<!-- Type Filter -->
<div class="filter-section">
<h3 class="filter-title">Vehicle Type</h3>
<div class="type-list">
<button class="type-item active" data-type="all">
<span class="type-icon">📦</span>
<span class="type-name">All Types</span>
</button>
<button class="type-item" data-type="car">
<span class="type-icon">🚗</span>
<span class="type-name">Cars</span>
</button>
<button class="type-item" data-type="truck">
<span class="type-icon">🚛</span>
<span class="type-name">Trucks</span>
</button>
<button class="type-item" data-type="air">
<span class="type-icon">🚁</span>
<span class="type-name">Aircraft</span>
</button>
<button class="type-item" data-type="sea">
<span class="type-icon">🚤</span>
<span class="type-name">Boats</span>
</button>
</div>
</div>
<!-- Search -->
<div class="filter-section">
<h3 class="filter-title">Search</h3>
<input type="text" class="search-input" id="searchInput" placeholder="Search vehicles...">
</div>
</div>
</div>
<!-- Center Panel - Vehicle Grid -->
<div class="garage-panel vehicles-panel">
<div class="panel-header">
<h2 class="panel-title">Your Vehicles</h2>
</div>
<div class="panel-content">
<div class="vehicles-grid" id="vehiclesGrid">
<!-- Vehicles will be dynamically generated -->
</div>
</div>
</div>
<!-- Right Panel - Vehicle Details -->
<div class="garage-panel details-panel" id="detailsPanel">
<div class="panel-header">
<h2 class="panel-title">Vehicle Details</h2>
</div>
<div class="panel-content">
<div class="no-selection" id="noSelection">
<div class="no-selection-icon">🚗</div>
<p>Select a vehicle to view details</p>
</div>
<div class="vehicle-details" id="vehicleDetails" style="display: none;">
<div class="detail-header">
<div class="detail-icon" id="detailIcon">🚗</div>
<div class="detail-info">
<h3 class="detail-name" id="detailName">Vehicle Name</h3>
<p class="detail-type" id="detailType">Type</p>
</div>
</div>
<div class="detail-stats">
<div class="detail-stat">
<span class="detail-label">Status</span>
<span class="detail-value" id="detailStatus">Stored</span>
</div>
<div class="detail-stat">
<span class="detail-label">Condition</span>
<span class="detail-value" id="detailCondition">100%</span>
</div>
<div class="detail-stat">
<span class="detail-label">Fuel</span>
<span class="detail-value" id="detailFuel">100%</span>
</div>
<div class="detail-stat">
<span class="detail-label">Location</span>
<span class="detail-value" id="detailLocation">Garage A</span>
</div>
</div>
<div class="detail-actions">
<button class="detail-btn spawn-btn" id="spawnBtn">
<span class="btn-icon">🚀</span>
<span class="btn-text">Spawn Vehicle</span>
</button>
<button class="detail-btn store-btn" id="storeBtn" style="display: none;">
<span class="btn-icon">📦</span>
<span class="btn-text">Store Vehicle</span>
</button>
</div>
<div class="detail-specs">
<h4 class="specs-title">Specifications</h4>
<div class="specs-list">
<div class="spec-item">
<span class="spec-label">Seats</span>
<span class="spec-value" id="detailSeats">4</span>
</div>
<div class="spec-item">
<span class="spec-label">Speed</span>
<span class="spec-value" id="detailSpeed">180 km/h</span>
</div>
<div class="spec-item">
<span class="spec-label">Cargo</span>
<span class="spec-value" id="detailCargo">200 kg</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="garage.js"></script>
</body>
</html>

View File

@ -0,0 +1,317 @@
/**
* Vehicle Garage Interface
* Handles vehicle management with spawn and store actions
*/
// Mock data - sample vehicles
const mockData = {
vehicles: [
// Cars
{ id: 1, name: "Sedan", type: "car", icon: "🚗", status: "stored", condition: 95, fuel: 80, location: "Garage A", seats: 4, speed: "180 km/h", cargo: "200 kg" },
{ id: 2, name: "Sports Car", type: "car", icon: "🏎️", status: "stored", condition: 100, fuel: 100, location: "Garage A", seats: 2, speed: "250 km/h", cargo: "50 kg" },
{ id: 3, name: "SUV", type: "car", icon: "🚙", status: "active", condition: 85, fuel: 60, location: "In Use", seats: 6, speed: "160 km/h", cargo: "400 kg" },
{ id: 4, name: "Hatchback", type: "car", icon: "🚗", status: "stored", condition: 90, fuel: 75, location: "Garage B", seats: 4, speed: "170 km/h", cargo: "250 kg" },
// Trucks
{ id: 5, name: "Pickup Truck", type: "truck", icon: "🚛", status: "stored", condition: 88, fuel: 70, location: "Garage A", seats: 2, speed: "140 km/h", cargo: "800 kg" },
{ id: 6, name: "Delivery Van", type: "truck", icon: "🚚", status: "stored", condition: 92, fuel: 85, location: "Garage B", seats: 3, speed: "130 km/h", cargo: "1200 kg" },
{ id: 7, name: "Heavy Truck", type: "truck", icon: "🚛", status: "active", condition: 75, fuel: 50, location: "In Use", seats: 2, speed: "120 km/h", cargo: "2000 kg" },
{ id: 8, name: "Box Truck", type: "truck", icon: "📦", status: "stored", condition: 80, fuel: 65, location: "Garage A", seats: 3, speed: "110 km/h", cargo: "1500 kg" },
// Aircraft
{ id: 9, name: "Helicopter", type: "air", icon: "🚁", status: "stored", condition: 95, fuel: 90, location: "Helipad", seats: 6, speed: "280 km/h", cargo: "500 kg" },
{ id: 10, name: "Light Plane", type: "air", icon: "✈️", status: "stored", condition: 100, fuel: 100, location: "Hangar", seats: 4, speed: "320 km/h", cargo: "300 kg" },
// Boats
{ id: 11, name: "Speedboat", type: "sea", icon: "🚤", status: "stored", condition: 93, fuel: 80, location: "Marina", seats: 4, speed: "100 km/h", cargo: "150 kg" },
{ id: 12, name: "Yacht", type: "sea", icon: "🛥️", status: "stored", condition: 98, fuel: 95, location: "Marina", seats: 12, speed: "60 km/h", cargo: "800 kg" }
]
};
// State
let selectedVehicle = null;
let statusFilter = 'all';
let typeFilter = 'all';
let searchQuery = '';
// Icons by type
const typeIcons = {
car: '🚗',
truck: '🚛',
air: '🚁',
sea: '🚤'
};
// Initialize
function initGarage() {
console.log('Garage interface initializing...');
setupEventHandlers();
renderVehicles();
updateStats();
console.log('Garage interface initialized');
}
// Event Handlers
function setupEventHandlers() {
// Close button
const closeBtn = document.querySelector('.close-btn');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
console.log('Closing garage...');
sendEvent('garage::close', {});
});
}
// Status filters
const filterBtns = document.querySelectorAll('.filter-btn');
filterBtns.forEach(btn => {
btn.addEventListener('click', () => {
filterBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
statusFilter = btn.dataset.filter;
renderVehicles();
});
});
// Type filters
const typeItems = document.querySelectorAll('.type-item');
typeItems.forEach(item => {
item.addEventListener('click', () => {
typeItems.forEach(i => i.classList.remove('active'));
item.classList.add('active');
typeFilter = item.dataset.type;
renderVehicles();
});
});
// Search
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
searchQuery = e.target.value.toLowerCase();
renderVehicles();
});
}
// Spawn button
const spawnBtn = document.getElementById('spawnBtn');
if (spawnBtn) {
spawnBtn.addEventListener('click', () => {
if (selectedVehicle) {
spawnVehicle(selectedVehicle);
}
});
}
// Store button
const storeBtn = document.getElementById('storeBtn');
if (storeBtn) {
storeBtn.addEventListener('click', () => {
if (selectedVehicle) {
storeVehicle(selectedVehicle);
}
});
}
}
// Render vehicles
function renderVehicles() {
const vehiclesGrid = document.getElementById('vehiclesGrid');
if (!vehiclesGrid) return;
vehiclesGrid.innerHTML = '';
// Filter vehicles
let filtered = mockData.vehicles;
// Status filter
if (statusFilter !== 'all') {
filtered = filtered.filter(v => v.status === statusFilter);
}
// Type filter
if (typeFilter !== 'all') {
filtered = filtered.filter(v => v.type === typeFilter);
}
// Search filter
if (searchQuery) {
filtered = filtered.filter(v =>
v.name.toLowerCase().includes(searchQuery) ||
v.type.toLowerCase().includes(searchQuery)
);
}
// Render vehicles
filtered.forEach(vehicle => {
const card = document.createElement('div');
card.className = 'vehicle-card';
if (selectedVehicle && selectedVehicle.id === vehicle.id) {
card.classList.add('selected');
}
card.innerHTML = `
<div class="vehicle-icon">${vehicle.icon}</div>
<div class="vehicle-name">${vehicle.name}</div>
<div class="vehicle-type">${vehicle.type}</div>
<div class="vehicle-status ${vehicle.status}">${vehicle.status}</div>
`;
card.addEventListener('click', () => selectVehicle(vehicle));
vehiclesGrid.appendChild(card);
});
console.log(`Rendered ${filtered.length} vehicles`);
}
// Select vehicle
function selectVehicle(vehicle) {
selectedVehicle = vehicle;
// Update selected state in grid
document.querySelectorAll('.vehicle-card').forEach(card => {
card.classList.remove('selected');
});
event.currentTarget.classList.add('selected');
// Show details
showVehicleDetails(vehicle);
}
// Show vehicle details
function showVehicleDetails(vehicle) {
const noSelection = document.getElementById('noSelection');
const vehicleDetails = document.getElementById('vehicleDetails');
const spawnBtn = document.getElementById('spawnBtn');
const storeBtn = document.getElementById('storeBtn');
if (noSelection) noSelection.style.display = 'none';
if (vehicleDetails) vehicleDetails.style.display = 'flex';
// Update details
document.getElementById('detailIcon').textContent = vehicle.icon;
document.getElementById('detailName').textContent = vehicle.name;
document.getElementById('detailType').textContent = vehicle.type;
document.getElementById('detailStatus').textContent = vehicle.status;
document.getElementById('detailCondition').textContent = `${vehicle.condition}%`;
document.getElementById('detailFuel').textContent = `${vehicle.fuel}%`;
document.getElementById('detailLocation').textContent = vehicle.location;
document.getElementById('detailSeats').textContent = vehicle.seats;
document.getElementById('detailSpeed').textContent = vehicle.speed;
document.getElementById('detailCargo').textContent = vehicle.cargo;
// Show/hide action buttons based on status
if (vehicle.status === 'stored') {
spawnBtn.style.display = 'flex';
storeBtn.style.display = 'none';
} else {
spawnBtn.style.display = 'none';
storeBtn.style.display = 'flex';
}
}
// Spawn vehicle
function spawnVehicle(vehicle) {
console.log('Spawning vehicle:', vehicle.name);
// Update local state
vehicle.status = 'active';
vehicle.location = 'In Use';
sendEvent('garage::spawn', {
vehicleId: vehicle.id,
vehicleName: vehicle.name,
vehicleType: vehicle.type
});
// Re-render
renderVehicles();
updateStats();
if (selectedVehicle && selectedVehicle.id === vehicle.id) {
showVehicleDetails(vehicle);
}
}
// Store vehicle
function storeVehicle(vehicle) {
console.log('Storing vehicle:', vehicle.name);
// Update local state
vehicle.status = 'stored';
vehicle.location = 'Garage A';
sendEvent('garage::store', {
vehicleId: vehicle.id,
vehicleName: vehicle.name,
vehicleType: vehicle.type
});
// Re-render
renderVehicles();
updateStats();
if (selectedVehicle && selectedVehicle.id === vehicle.id) {
showVehicleDetails(vehicle);
}
}
// Update stats
function updateStats() {
const stored = mockData.vehicles.filter(v => v.status === 'stored').length;
const active = mockData.vehicles.filter(v => v.status === 'active').length;
const capacity = mockData.vehicles.length + 6; // Mock capacity
document.getElementById('storedCount').textContent = stored;
document.getElementById('activeCount').textContent = active;
document.getElementById('capacityCount').textContent = capacity;
}
// Update garage data from external source
function updateGarageData(data) {
if (data.vehicles) {
mockData.vehicles = data.vehicles;
renderVehicles();
updateStats();
// Update selected vehicle if it still exists
if (selectedVehicle) {
const updated = mockData.vehicles.find(v => v.id === selectedVehicle.id);
if (updated) {
selectedVehicle = updated;
showVehicleDetails(updated);
} else {
selectedVehicle = null;
const noSelection = document.getElementById('noSelection');
const vehicleDetails = document.getElementById('vehicleDetails');
if (noSelection) noSelection.style.display = 'flex';
if (vehicleDetails) vehicleDetails.style.display = 'none';
}
}
}
}
// Send event to Arma
function sendEvent(event, data) {
if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify({
event: event,
data: data
}));
} else {
console.log('Event:', event, 'Data:', data);
}
}
// Auto-initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initGarage);
} else {
initGarage();
}
// Expose functions globally
window.updateGarageData = updateGarageData;
window.selectVehicle = selectVehicle;
window.spawnVehicle = spawnVehicle;
window.storeVehicle = storeVehicle;

View File

@ -0,0 +1,70 @@
<!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" />
<!--
Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API
This approach is used instead of static HTML imports to work with Arma 3's file system
-->
<script>
Promise.all([
A3API.RequestFile(
"forge\\forge_client\\addons\\actor\\ui\\_site\\style.css",
),
A3API.RequestFile(
"forge\\forge_client\\addons\\actor\\ui\\_site\\script.js",
),
]).then(([css, js]) => {
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
const script = document.createElement("script");
script.text = js;
document.head.appendChild(script);
});
</script>
</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>
</body>
</html>

View File

@ -0,0 +1,424 @@
/**
* 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",
},
{
id: "org",
title: "Organization Dashboard",
description: "View and manage your organization data",
icon: "",
action: "actor::open::org",
},
];
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

@ -0,0 +1,417 @@
/**
* 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

@ -0,0 +1,567 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 100vh;
width: 100vw;
background: rgba(0, 0, 0, 0.7);
font-family: Arial, sans-serif;
color: rgba(200, 220, 240, 0.95);
overflow: hidden;
}
.store-container {
height: 100vh;
width: 100vw;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Header Section */
.store-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);
}
.store-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;
}
.store-info {
flex: 1;
}
.store-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;
}
.store-subtitle {
font-size: 0.875rem;
color: rgba(140, 160, 180, 0.8);
letter-spacing: 0.5px;
}
.balance-display {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.25rem;
padding: 0.75rem 1.25rem;
background: rgba(20, 30, 45, 0.7);
border: 1px solid rgba(100, 200, 150, 0.4);
border-radius: 4px;
}
.balance-label {
font-size: 0.7rem;
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);
}
.header-actions {
display: flex;
gap: 0.75rem;
}
.action-btn {
padding: 0.625rem 1.25rem;
background: rgba(20, 30, 45, 0.7);
border: 1px solid rgba(100, 150, 200, 0.4);
border-radius: 4px;
color: rgba(200, 220, 240, 0.95);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
transition: all 0.15s ease;
}
.action-btn:hover {
background: rgba(30, 45, 70, 0.9);
border-color: rgba(150, 200, 255, 0.7);
box-shadow:
0 0 15px rgba(100, 150, 200, 0.2),
inset 0 0 20px rgba(100, 150, 200, 0.05);
}
.cart-btn {
display: flex;
align-items: center;
gap: 0.5rem;
position: relative;
}
.cart-icon {
font-size: 1.25rem;
}
.cart-count {
min-width: 24px;
height: 24px;
padding: 0 0.5rem;
background: rgba(100, 150, 200, 0.3);
border: 1px solid rgba(100, 150, 200, 0.5);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
}
.close-btn {
border-color: rgba(200, 100, 100, 0.4);
}
.close-btn:hover {
border-color: rgba(255, 100, 100, 0.7);
box-shadow:
0 0 15px rgba(200, 100, 100, 0.2),
inset 0 0 20px rgba(200, 100, 100, 0.05);
}
/* Main Content */
.store-content {
flex: 1;
display: grid;
grid-template-columns: 250px 1fr;
gap: 1.5rem;
overflow: hidden;
}
.store-content.cart-open {
grid-template-columns: 250px 1fr 350px;
}
/* Panels */
.store-panel {
background: rgba(15, 20, 30, 0.9);
border: 1px solid rgba(100, 150, 200, 0.4);
border-left: 3px solid rgba(100, 150, 200, 0.5);
border-radius: 4px;
display: flex;
flex-direction: column;
box-shadow:
0 0 20px rgba(100, 150, 200, 0.1),
0 4px 16px rgba(0, 0, 0, 0.6);
}
.panel-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid rgba(100, 150, 200, 0.2);
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.panel-title {
font-size: 1rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(200, 220, 255, 1);
}
.panel-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
}
/* Custom Scrollbar */
.panel-content::-webkit-scrollbar {
width: 8px;
}
.panel-content::-webkit-scrollbar-track {
background: rgba(15, 20, 30, 0.5);
border-radius: 4px;
}
.panel-content::-webkit-scrollbar-thumb {
background: rgba(100, 150, 200, 0.3);
border-radius: 4px;
}
.panel-content::-webkit-scrollbar-thumb:hover {
background: rgba(100, 150, 200, 0.5);
}
/* Search Box */
.search-box {
flex: 1;
max-width: 300px;
}
.search-input {
width: 100%;
padding: 0.625rem 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;
}
.search-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);
}
.search-input::placeholder {
color: rgba(100, 120, 140, 0.6);
}
/* Category List */
.category-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.category-item {
display: flex;
align-items: center;
gap: 0.75rem;
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;
color: rgba(200, 220, 240, 0.95);
cursor: pointer;
transition: all 0.15s ease;
text-align: left;
}
.category-item:hover {
background: rgba(30, 45, 70, 0.7);
border-left-color: rgba(150, 200, 255, 0.7);
}
.category-item.active {
background: rgba(30, 45, 70, 0.8);
border-left-color: rgba(100, 200, 150, 0.8);
box-shadow:
0 0 15px rgba(100, 200, 150, 0.15),
inset 0 0 20px rgba(100, 200, 150, 0.05);
}
.category-icon {
font-size: 1.5rem;
}
.category-name {
flex: 1;
font-size: 0.875rem;
font-weight: 500;
}
.category-count {
padding: 0.25rem 0.5rem;
background: rgba(100, 150, 200, 0.2);
border: 1px solid rgba(100, 150, 200, 0.3);
border-radius: 3px;
font-size: 0.75rem;
font-weight: 600;
}
/* Items Grid */
.items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.item-card {
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;
padding: 1.25rem;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.item-card:hover {
background: rgba(30, 45, 70, 0.7);
border-left-color: rgba(150, 200, 255, 0.7);
box-shadow:
0 0 15px rgba(100, 150, 200, 0.15),
inset 0 0 20px rgba(100, 150, 200, 0.05);
}
.item-icon {
font-size: 3rem;
text-align: center;
}
.item-name {
font-size: 0.95rem;
font-weight: 600;
color: rgba(200, 220, 255, 1);
text-align: center;
}
.item-description {
font-size: 0.75rem;
color: rgba(140, 160, 180, 0.85);
text-align: center;
line-height: 1.3;
min-height: 2.6rem;
}
.item-price {
font-size: 1.125rem;
font-weight: 600;
color: rgba(100, 200, 150, 1);
text-align: center;
margin-top: auto;
}
.item-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
.add-to-cart-btn {
flex: 1;
padding: 0.625rem;
background: rgba(100, 150, 200, 0.2);
border: 1px solid rgba(100, 150, 200, 0.4);
border-radius: 4px;
color: rgba(200, 220, 240, 0.95);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
transition: all 0.15s ease;
}
.add-to-cart-btn:hover {
background: rgba(100, 150, 200, 0.3);
border-color: rgba(150, 200, 255, 0.6);
}
/* Cart Panel */
.cart-items {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.empty-cart {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 3rem 1rem;
}
.empty-icon {
font-size: 3rem;
opacity: 0.3;
}
.empty-text {
font-size: 0.875rem;
color: rgba(140, 160, 180, 0.7);
}
.cart-item {
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;
}
.cart-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.cart-item-name {
font-size: 0.875rem;
font-weight: 600;
color: rgba(200, 220, 255, 1);
}
.cart-item-remove {
padding: 0.25rem 0.5rem;
background: rgba(200, 100, 100, 0.2);
border: 1px solid rgba(200, 100, 100, 0.4);
border-radius: 3px;
color: rgba(255, 150, 150, 0.9);
font-size: 0.7rem;
cursor: pointer;
transition: all 0.15s ease;
}
.cart-item-remove:hover {
background: rgba(200, 100, 100, 0.3);
}
.cart-item-details {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
color: rgba(160, 180, 200, 0.85);
}
.cart-item-price {
color: rgba(100, 200, 150, 1);
font-weight: 600;
}
.clear-cart-btn {
padding: 0.5rem 0.75rem;
background: rgba(200, 100, 100, 0.2);
border: 1px solid rgba(200, 100, 100, 0.4);
border-radius: 4px;
color: rgba(255, 150, 150, 0.9);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
transition: all 0.15s ease;
}
.clear-cart-btn:hover {
background: rgba(200, 100, 100, 0.3);
}
/* Cart Summary */
.cart-summary {
padding-top: 1.5rem;
border-top: 1px solid rgba(100, 150, 200, 0.2);
}
.summary-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.summary-label {
font-size: 0.875rem;
color: rgba(160, 180, 200, 0.85);
}
.summary-value {
font-size: 0.95rem;
font-weight: 600;
color: rgba(200, 220, 240, 0.95);
}
.summary-total {
padding-top: 0.75rem;
border-top: 1px solid rgba(100, 150, 200, 0.2);
margin-top: 0.5rem;
}
.summary-total .summary-label {
font-size: 1rem;
font-weight: 600;
color: rgba(200, 220, 255, 1);
}
.summary-total .summary-value {
font-size: 1.25rem;
color: rgba(100, 200, 150, 1);
}
.action-btn-primary {
width: 100%;
padding: 0.875rem;
margin-top: 1rem;
background: rgba(100, 150, 200, 0.2);
border: 1px solid rgba(100, 150, 200, 0.5);
}
.action-btn-primary:hover {
background: rgba(100, 150, 200, 0.3);
border-color: rgba(150, 200, 255, 0.7);
}
.checkout-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.checkout-btn:disabled:hover {
background: rgba(100, 150, 200, 0.2);
border-color: rgba(100, 150, 200, 0.5);
box-shadow: none;
}
/* Responsive adjustments */
@media (max-width: 1400px) {
.items-grid {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}
}
@media (max-width: 1200px) {
.store-content {
grid-template-columns: 1fr;
}
.store-content.cart-open {
grid-template-columns: 1fr 350px;
}
.categories-panel {
display: none;
}
}

View File

@ -0,0 +1,149 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Store</title>
<link rel="stylesheet" href="store.css" />
<!--
Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API
This approach is used instead of static HTML imports to work with Arma 3's file system
-->
<script>
Promise.all([
A3API.RequestFile(
"forge\\forge_client\\addons\\actor\\ui\\_site\\store.css",
),
A3API.RequestFile(
"forge\\forge_client\\addons\\actor\\ui\\_site\\store.js",
),
]).then(([css, js]) => {
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
const script = document.createElement("script");
script.text = js;
document.head.appendChild(script);
});
</script>
</head>
<body>
<div class="store-container">
<!-- Header Section -->
<div class="store-header">
<div class="store-logo">
<div class="logo-icon">🛒</div>
</div>
<div class="store-info">
<h1 class="store-title">Supply Store</h1>
<p class="store-subtitle">Equipment & Resources</p>
</div>
<div class="balance-display">
<span class="balance-label">Available Funds</span>
<span class="balance-amount">$45,750</span>
</div>
<div class="header-actions">
<button class="action-btn cart-btn" id="cartToggle">
<span class="cart-icon">🛒</span>
<span class="cart-count">0</span>
</button>
<button class="action-btn close-btn">Close</button>
</div>
</div>
<!-- Main Content -->
<div class="store-content">
<!-- Left Panel - Categories -->
<div class="store-panel categories-panel">
<div class="panel-header">
<h2 class="panel-title">Categories</h2>
</div>
<div class="panel-content">
<div class="category-list">
<button class="category-item active" data-category="all">
<span class="category-icon">📦</span>
<span class="category-name">All Items</span>
<span class="category-count">24</span>
</button>
<button class="category-item" data-category="weapons">
<span class="category-icon">🔫</span>
<span class="category-name">Weapons</span>
<span class="category-count">8</span>
</button>
<button class="category-item" data-category="equipment">
<span class="category-icon">🎽</span>
<span class="category-name">Equipment</span>
<span class="category-count">6</span>
</button>
<button class="category-item" data-category="medical">
<span class="category-icon">💊</span>
<span class="category-name">Medical</span>
<span class="category-count">5</span>
</button>
<button class="category-item" data-category="supplies">
<span class="category-icon">📦</span>
<span class="category-name">Supplies</span>
<span class="category-count">5</span>
</button>
</div>
</div>
</div>
<!-- Center Panel - Items Grid -->
<div class="store-panel items-panel">
<div class="panel-header">
<h2 class="panel-title">Available Items</h2>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search items..." id="searchInput">
</div>
</div>
<div class="panel-content">
<div class="items-grid" id="itemsGrid">
<!-- Items will be dynamically generated -->
</div>
</div>
</div>
<!-- Right Panel - Cart (Initially Hidden) -->
<div class="store-panel cart-panel" id="cartPanel" style="display: none;">
<div class="panel-header">
<h2 class="panel-title">Shopping Cart</h2>
<button class="clear-cart-btn" id="clearCart">Clear</button>
</div>
<div class="panel-content">
<div class="cart-items" id="cartItems">
<div class="empty-cart">
<span class="empty-icon">🛒</span>
<span class="empty-text">Your cart is empty</span>
</div>
</div>
<div class="cart-summary">
<div class="summary-row">
<span class="summary-label">Subtotal</span>
<span class="summary-value" id="cartSubtotal">$0</span>
</div>
<div class="summary-row">
<span class="summary-label">Tax (5%)</span>
<span class="summary-value" id="cartTax">$0</span>
</div>
<div class="summary-row summary-total">
<span class="summary-label">Total</span>
<span class="summary-value" id="cartTotal">$0</span>
</div>
<button class="action-btn action-btn-primary checkout-btn" id="checkoutBtn">
Complete Purchase
</button>
</div>
</div>
</div>
</div>
</div>
<script src="store.js"></script>
</body>
</html>

View File

@ -0,0 +1,339 @@
/**
* Store Interface
* Handles item browsing, cart management, and purchases
*/
// Mock data
const mockData = {
balance: 45750,
items: [
// Weapons
{ id: 1, name: "Assault Rifle", category: "weapons", icon: "🔫", description: "Standard military-grade rifle", price: 2500 },
{ id: 2, name: "Sniper Rifle", category: "weapons", icon: "🎯", description: "Long-range precision weapon", price: 4500 },
{ id: 3, name: "SMG", category: "weapons", icon: "🔫", description: "Close-quarters combat", price: 1800 },
{ id: 4, name: "Pistol", category: "weapons", icon: "🔫", description: "Sidearm backup weapon", price: 800 },
{ id: 5, name: "Shotgun", category: "weapons", icon: "🔫", description: "Close-range powerhouse", price: 1500 },
{ id: 6, name: "LMG", category: "weapons", icon: "🔫", description: "Heavy suppression weapon", price: 3500 },
{ id: 7, name: "Grenade Launcher", category: "weapons", icon: "💣", description: "Explosive ordnance", price: 5000 },
{ id: 8, name: "Rocket Launcher", category: "weapons", icon: "🚀", description: "Anti-vehicle weapon", price: 8000 },
// Equipment
{ id: 9, name: "Body Armor", category: "equipment", icon: "🎽", description: "Ballistic protection", price: 3000 },
{ id: 10, name: "Helmet", category: "equipment", icon: "⛑️", description: "Head protection", price: 1200 },
{ id: 11, name: "Night Vision", category: "equipment", icon: "🕶️", description: "See in the dark", price: 2500 },
{ id: 12, name: "GPS Device", category: "equipment", icon: "📡", description: "Navigation system", price: 800 },
{ id: 13, name: "Radio", category: "equipment", icon: "📻", description: "Team communication", price: 600 },
{ id: 14, name: "Backpack", category: "equipment", icon: "🎒", description: "Extra storage capacity", price: 500 },
// Medical
{ id: 15, name: "First Aid Kit", category: "medical", icon: "💊", description: "Basic medical supplies", price: 400 },
{ id: 16, name: "Med Kit", category: "medical", icon: "⚕️", description: "Advanced medical kit", price: 1000 },
{ id: 17, name: "Bandages", category: "medical", icon: "🩹", description: "Stop bleeding", price: 150 },
{ id: 18, name: "Morphine", category: "medical", icon: "💉", description: "Pain management", price: 300 },
{ id: 19, name: "Blood Bag", category: "medical", icon: "🩸", description: "Restore blood level", price: 500 },
// Supplies
{ id: 20, name: "Ammunition Box", category: "supplies", icon: "📦", description: "Mixed ammunition", price: 800 },
{ id: 21, name: "Explosive Charges", category: "supplies", icon: "💣", description: "Demolition supplies", price: 1500 },
{ id: 22, name: "Toolkit", category: "supplies", icon: "🔧", description: "Repair equipment", price: 600 },
{ id: 23, name: "Food Rations", category: "supplies", icon: "🥫", description: "Emergency supplies", price: 200 },
{ id: 24, name: "Water Canteen", category: "supplies", icon: "🧃", description: "Hydration supply", price: 150 }
]
};
// State
let cart = [];
let selectedCategory = 'all';
let searchQuery = '';
// Initialize
function initStore() {
console.log('Store interface initializing...');
setupEventHandlers();
renderItems();
updateBalance();
console.log('Store interface initialized');
}
// Event Handlers
function setupEventHandlers() {
// Close button
const closeBtn = document.querySelector('.close-btn');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
console.log('Closing store...');
sendEvent('actor::close::store', {});
});
}
// Cart toggle
const cartToggle = document.getElementById('cartToggle');
const cartPanel = document.getElementById('cartPanel');
const storeContent = document.querySelector('.store-content');
if (cartToggle && cartPanel) {
cartToggle.addEventListener('click', () => {
const isOpen = cartPanel.style.display !== 'none';
cartPanel.style.display = isOpen ? 'none' : 'flex';
storeContent.classList.toggle('cart-open', !isOpen);
});
}
// Category filters
const categoryItems = document.querySelectorAll('.category-item');
categoryItems.forEach(item => {
item.addEventListener('click', () => {
categoryItems.forEach(c => c.classList.remove('active'));
item.classList.add('active');
selectedCategory = item.dataset.category;
renderItems();
});
});
// Search
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
searchQuery = e.target.value.toLowerCase();
renderItems();
});
}
// Clear cart
const clearCartBtn = document.getElementById('clearCart');
if (clearCartBtn) {
clearCartBtn.addEventListener('click', () => {
if (confirm('Clear all items from cart?')) {
cart = [];
renderCart();
updateCartCount();
}
});
}
// Checkout
const checkoutBtn = document.getElementById('checkoutBtn');
if (checkoutBtn) {
checkoutBtn.addEventListener('click', handleCheckout);
}
}
// Render items
function renderItems() {
const itemsGrid = document.getElementById('itemsGrid');
if (!itemsGrid) return;
itemsGrid.innerHTML = '';
// Filter items
let filteredItems = mockData.items;
if (selectedCategory !== 'all') {
filteredItems = filteredItems.filter(item => item.category === selectedCategory);
}
if (searchQuery) {
filteredItems = filteredItems.filter(item =>
item.name.toLowerCase().includes(searchQuery) ||
item.description.toLowerCase().includes(searchQuery)
);
}
// Render filtered items
filteredItems.forEach(item => {
const card = document.createElement('div');
card.className = 'item-card';
card.innerHTML = `
<div class="item-icon">${item.icon}</div>
<div class="item-name">${item.name}</div>
<div class="item-description">${item.description}</div>
<div class="item-price">$${item.price.toLocaleString()}</div>
<div class="item-actions">
<button class="add-to-cart-btn" data-item-id="${item.id}">Add to Cart</button>
</div>
`;
const addBtn = card.querySelector('.add-to-cart-btn');
addBtn.addEventListener('click', (e) => {
e.stopPropagation();
addToCart(item);
});
itemsGrid.appendChild(card);
});
console.log(`Rendered ${filteredItems.length} items`);
}
// Cart functions
function addToCart(item) {
const existingItem = cart.find(c => c.id === item.id);
if (existingItem) {
existingItem.quantity++;
} else {
cart.push({ ...item, quantity: 1 });
}
renderCart();
updateCartCount();
// Show cart panel if not visible
const cartPanel = document.getElementById('cartPanel');
const storeContent = document.querySelector('.store-content');
if (cartPanel.style.display === 'none') {
cartPanel.style.display = 'flex';
storeContent.classList.add('cart-open');
}
console.log('Added to cart:', item.name);
}
function removeFromCart(itemId) {
cart = cart.filter(item => item.id !== itemId);
renderCart();
updateCartCount();
}
function renderCart() {
const cartItems = document.getElementById('cartItems');
if (!cartItems) return;
cartItems.innerHTML = '';
if (cart.length === 0) {
cartItems.innerHTML = `
<div class="empty-cart">
<span class="empty-icon">🛒</span>
<span class="empty-text">Your cart is empty</span>
</div>
`;
} else {
cart.forEach(item => {
const cartItem = document.createElement('div');
cartItem.className = 'cart-item';
cartItem.innerHTML = `
<div class="cart-item-header">
<span class="cart-item-name">${item.name}</span>
<button class="cart-item-remove" data-item-id="${item.id}">Remove</button>
</div>
<div class="cart-item-details">
<span>Qty: ${item.quantity}</span>
<span class="cart-item-price">$${(item.price * item.quantity).toLocaleString()}</span>
</div>
`;
const removeBtn = cartItem.querySelector('.cart-item-remove');
removeBtn.addEventListener('click', () => removeFromCart(item.id));
cartItems.appendChild(cartItem);
});
}
updateCartSummary();
}
function updateCartCount() {
const cartCount = document.querySelector('.cart-count');
if (cartCount) {
const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
cartCount.textContent = totalItems;
}
}
function updateCartSummary() {
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const tax = subtotal * 0.05;
const total = subtotal + tax;
document.getElementById('cartSubtotal').textContent = `$${subtotal.toLocaleString()}`;
document.getElementById('cartTax').textContent = `$${tax.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
document.getElementById('cartTotal').textContent = `$${total.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const checkoutBtn = document.getElementById('checkoutBtn');
if (checkoutBtn) {
checkoutBtn.disabled = cart.length === 0 || total > mockData.balance;
}
}
function handleCheckout() {
const total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const tax = total * 0.05;
const grandTotal = total + tax;
if (grandTotal > mockData.balance) {
alert('Insufficient funds!');
return;
}
const purchaseData = {
items: cart.map(item => ({
id: item.id,
name: item.name,
quantity: item.quantity,
price: item.price
})),
subtotal: total,
tax: tax,
total: grandTotal
};
console.log('Purchase request:', purchaseData);
sendEvent('actor::store::purchase', purchaseData);
// Clear cart after purchase
cart = [];
renderCart();
updateCartCount();
// Update balance (this would normally come from server)
mockData.balance -= grandTotal;
updateBalance();
}
function updateBalance() {
const balanceAmount = document.querySelector('.balance-amount');
if (balanceAmount) {
balanceAmount.textContent = `$${mockData.balance.toLocaleString()}`;
}
}
// Update store data from external source
function updateStoreData(data) {
if (data.balance !== undefined) {
mockData.balance = data.balance;
updateBalance();
}
if (data.items) {
mockData.items = data.items;
renderItems();
}
}
// Send event to Arma
function sendEvent(event, data) {
if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify({
event: event,
data: data
}));
} else {
console.log('Event:', event, 'Data:', data);
}
}
// Auto-initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initStore);
} else {
initStore();
}
// Expose functions globally
window.updateStoreData = updateStoreData;
window.addToCart = addToCart;

View File

@ -0,0 +1,116 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 100vh;
width: 100vw;
background: rgba(0, 0, 0, 0.5);
font-family: Arial, sans-serif;
}
.container {
align-items: flex-end;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
padding-right: 5%;
perspective: 1200px;
}
.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);
.neu-menu-content {
height: 100%;
overflow: hidden;
padding: 1rem;
.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;
}
}
}
}
}

View File

@ -0,0 +1,186 @@
* {
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

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

View File

@ -0,0 +1,19 @@
class Extended_PreStart_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
};
};
class Extended_PreInit_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_preInitClient));
};
};
class Extended_PostInit_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_postInit));
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_postInitClient));
};
};

View File

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

View File

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

View File

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

View File

@ -0,0 +1,28 @@
#include "script_component.hpp"
if (isNil QGVAR(BankClass)) then { [] call FUNC(initBankClass); };
[QGVAR(initBank), {
GVAR(BankClass) call ["init", []];
}] call CFUNC(addEventHandler);
[QGVAR(responseInitBank), {
params [["_data", createHashMap, [createHashMap]]];
GVAR(BankClass) call ["sync", [_data, true]];
SETPVAR(player,FORGE_isLoaded,true);
cutText ["", "PLAIN", 1];
}] call CFUNC(addEventHandler);
[QGVAR(responseSyncBank), {
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
GVAR(BankClass) call ["sync", [_data, _jip]];
}] call CFUNC(addEventHandler);
[{
EGVAR(org,OrgClass) get "isLoaded";
}, {
[QGVAR(initBank), []] call CFUNC(localEvent);
}] call CFUNC(waitUntilAndExecute);

View File

@ -0,0 +1,10 @@
#include "script_component.hpp"
PREP_RECOMPILE_START;
#include "XEH_PREP.hpp"
PREP_RECOMPILE_END;
// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
#include "initSettings.inc.sqf"
#include "initKeybinds.inc.sqf"

View File

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

View File

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

View File

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

View File

@ -0,0 +1,33 @@
#include "..\script_component.hpp"
/*
* Author: IDSolutions
* Handles the UI events.
*
* Arguments:
* None
*
* Return Value:
* None
*
* Example:
* [] call forge_client_bank_fnc_handleUIEvents;
*
* Public: No
*/
params ["_control", "_isConfirmDialog", "_message"];
private _alert = fromJSON _message;
private _event = _alert get "event";
private _data = _alert get "data";
private _display = displayChild findDisplay 46;
diag_log format ["[FORGE:Client:Bank] Handling UI event: %1 with data: %2", _event, _data];
switch (_event) do {
case "bank::close": { _display closeDisplay 1; };
default { hint format ["Unhandled UI event: %1", _event]; };
};
true;

View File

@ -0,0 +1,88 @@
#include "..\script_component.hpp"
/*
* Author: IDSolutions
* Initializes the bank class.
*
* Arguments:
* None
*
* Return Value:
* None
*
* Examples:
* [] call forge_client_bank_fnc_initBankClass
*
* Public: Yes
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(BankClass) = createHashMapObject [[
["#type", "IBankClass"],
["#create", {
_self set ["uid", getPlayerUID player];
_self set ["account", createHashMap];
_self set ["isLoaded", false];
_self set ["lastSave", time];
private _actor = EGVAR(actor,ActorClass) get "actor";
private _phone_number = _actor get "phone_number";
private _email = _actor get "email";
private _account = createHashMap;
_account set ["uid", (getPlayerUID player)];
_account set ["name", (name player)];
_account set ["bank", 0];
_account set ["cash", 0];
_account set ["earnings", 0];
_account set ["transactions", []];
_account set ["phone_number", _phone_number];
_account set ["email", _email];
_self set ["account", _account];
}],
["init", {
private _uid = _self get "uid";
private _account = _self get "account";
[SRPC(bank,requestInitBank), [_uid, _account]] call CFUNC(serverEvent);
systemChat format ["Bank loaded for %1", (name player)];
diag_log "[FORGE:Client:Bank] Bank Class Initialized!";
}],
["save", {
params [["_sync", false, [false]]];
private _uid = _self get "uid";
[SRPC(bank,requestSaveBank), [_uid, _sync]] call CFUNC(serverEvent);
_self set ["lastSave", time];
}],
["sync", {
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
private _account = _self get "account";
private _isLoaded = _self get "isLoaded";
if !(_isLoaded) then { _self set ["isLoaded", true]; };
if (_data isEqualTo createHashMap) exitWith {
diag_log "[FORGE:Client:Bank] Empty data received for sync, skipping.";
};
{
_account set [_x, _y];
} forEach _data;
_self set ["account", _account];
diag_log "[FORGE:Client:Bank] Sync completed";
}],
["get", {
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
private _account = _self get "account";
_account getOrDefault [_key, _default];
}]
]];
SETVAR(player,FORGE_BankClass,GVAR(BankClass));
GVAR(BankClass)

View File

@ -0,0 +1,31 @@
#include "..\script_component.hpp"
/*
* Author: IDSolutions
* Opens the player bank interaction interface.
*
* Arguments:
* None
*
* Return Value:
* None
*
* Example:
* [] call forge_client_bank_fnc_openUI;
*
* Public: No
*/
private _display = (findDisplay 46) createDisplay "RscBank";
private _ctrl = (_display displayCtrl 1002);
_ctrl ctrlAddEventHandler ["JSDialog", {
params ["_control", "_isConfirmDialog", "_message"];
[_control, _isConfirmDialog, _message] call FUNC(handleUIEvents);
}];
_ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\index.html)];
// _ctrl ctrlWebBrowserAction ["OpenDevConsole"];
true;

View File

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

View File

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

View File

@ -0,0 +1,9 @@
#define COMPONENT bank
#define COMPONENT_BEAUTIFIED Bank
#include "\forge\forge_client\addons\main\script_mod.hpp"
// #define DEBUG_MODE_FULL
// #define DISABLE_COMPILE_CACHE
// #define ENABLE_PERFORMANCE_COUNTERS
#include "\forge\forge_client\addons\main\script_macros.hpp"

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project name="FFE">
<Package name="Bank">
<Key ID="STR_forge_client_bank_displayName">
<English>Bank</English>
</Key>
</Package>
</Project>

View File

@ -0,0 +1,21 @@
class RscBank {
idd = 1001;
fadeIn = 0;
fadeOut = 0;
duration = 1e011;
onLoad = "uiNamespace setVariable ['RscBank', _this select 0]";
onUnLoad = "uinamespace setVariable ['RscBank', nil]";
class controlsBackground {};
class controls {
class IFrame: RscText {
type = 106;
idc = 1002;
x = "safeZoneXAbs";
y = "safeZoneY";
w = "safeZoneWAbs";
h = "safeZoneH";
colorBackground[] = {0, 0, 0, 0};
};
};
};

View File

@ -0,0 +1,98 @@
// Control types
#define CT_STATIC 0
#define CT_BUTTON 1
#define CT_EDIT 2
#define CT_SLIDER 3
#define CT_COMBO 4
#define CT_LISTBOX 5
#define CT_TOOLBOX 6
#define CT_CHECKBOXES 7
#define CT_PROGRESS 8
#define CT_HTML 9
#define CT_STATIC_SKEW 10
#define CT_ACTIVETEXT 11
#define CT_TREE 12
#define CT_STRUCTURED_TEXT 13
#define CT_CONTEXT_MENU 14
#define CT_CONTROLS_GROUP 15
#define CT_SHORTCUTBUTTON 16
#define CT_HITZONES 17
#define CT_XKEYDESC 40
#define CT_XBUTTON 41
#define CT_XLISTBOX 42
#define CT_XSLIDER 43
#define CT_XCOMBO 44
#define CT_ANIMATED_TEXTURE 45
#define CT_OBJECT 80
#define CT_OBJECT_ZOOM 81
#define CT_OBJECT_CONTAINER 82
#define CT_OBJECT_CONT_ANIM 83
#define CT_LINEBREAK 98
#define CT_USER 99
#define CT_MAP 100
#define CT_MAP_MAIN 101
#define CT_LISTNBOX 102
#define CT_ITEMSLOT 103
#define CT_CHECKBOX 77
// Static styles
#define ST_POS 0x0F
#define ST_HPOS 0x03
#define ST_VPOS 0x0C
#define ST_LEFT 0x00
#define ST_RIGHT 0x01
#define ST_CENTER 0x02
#define ST_DOWN 0x04
#define ST_UP 0x08
#define ST_VCENTER 0x0C
#define ST_TYPE 0xF0
#define ST_SINGLE 0x00
#define ST_MULTI 0x10
#define ST_TITLE_BAR 0x20
#define ST_PICTURE 0x30
#define ST_FRAME 0x40
#define ST_BACKGROUND 0x50
#define ST_GROUP_BOX 0x60
#define ST_GROUP_BOX2 0x70
#define ST_HUD_BACKGROUND 0x80
#define ST_TILE_PICTURE 0x90
#define ST_WITH_RECT 0xA0
#define ST_LINE 0xB0
#define ST_UPPERCASE 0xC0
#define ST_LOWERCASE 0xD0
#define ST_SHADOW 0x100
#define ST_NO_RECT 0x200
#define ST_KEEP_ASPECT_RATIO 0x800
// Slider styles
#define SL_DIR 0x400
#define SL_VERT 0
#define SL_HORZ 0x400
#define SL_TEXTURES 0x10
// progress bar
#define ST_VERTICAL 0x01
#define ST_HORIZONTAL 0
// Listbox styles
#define LB_TEXTURES 0x10
#define LB_MULTI 0x20
// Tree styles
#define TR_SHOWROOT 1
#define TR_AUTOCOLLAPSE 2
// Default text sizes
#define GUI_TEXT_SIZE_SMALL (GUI_GRID_H * 0.8)
#define GUI_TEXT_SIZE_MEDIUM (GUI_GRID_H * 1)
#define GUI_TEXT_SIZE_LARGE (GUI_GRID_H * 1.2)
// Pixel grid
#define pixelScale 0.50
#define GRID_W (pixelW * pixelGrid * pixelScale)
#define GRID_H (pixelH * pixelGrid * pixelScale)
class RscText;

View File

@ -0,0 +1,585 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
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;
}
.atm-container {
height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 5%;
perspective: 1200px;
}
.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%;
}
/* 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);
text-align: center;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(100, 150, 200, 0.2);
}
/* 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;
}
.pin-display {
display: flex;
justify-content: center;
gap: 1rem;
padding: 1.5rem;
}
.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 {
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;
}
.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;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
cursor: pointer;
transition: all 0.15s ease;
}
.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 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.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 {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.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 {
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 {
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;
cursor: pointer;
transition: all 0.15s 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;
}
.atm-screen {
transform: none;
width: 100%;
max-width: 450px;
}
}

View File

@ -0,0 +1,255 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ATM</title>
<link rel="stylesheet" href="atm.css" />
<!--
Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API
This approach is used instead of static HTML imports to work with Arma 3's file system
-->
<script>
Promise.all([
A3API.RequestFile(
"forge\\forge_client\\addons\\actor\\ui\\_site\\atm.css",
),
A3API.RequestFile(
"forge\\forge_client\\addons\\actor\\ui\\_site\\atm.js",
),
]).then(([css, js]) => {
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
const script = document.createElement("script");
script.text = js;
document.head.appendChild(script);
});
</script>
</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">
<button class="key-btn" onclick="enterPin('1')">1</button>
<button class="key-btn" onclick="enterPin('2')">2</button>
<button class="key-btn" onclick="enterPin('3')">3</button>
<button class="key-btn" onclick="enterPin('4')">4</button>
<button class="key-btn" onclick="enterPin('5')">5</button>
<button class="key-btn" onclick="enterPin('6')">6</button>
<button class="key-btn" onclick="enterPin('7')">7</button>
<button class="key-btn" onclick="enterPin('8')">8</button>
<button class="key-btn" onclick="enterPin('9')">9</button>
<button class="key-btn key-clear" onclick="clearPin()">Clear</button>
<button class="key-btn" onclick="enterPin('0')">0</button>
<button class="key-btn key-enter" onclick="submitPin()">Enter</button>
</div>
</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-icon">💵</span> -->
<span class="menu-text">Withdraw</span>
</button>
<button class="menu-btn" onclick="showView('depositView')">
<!-- <span class="menu-icon">💰</span> -->
<span class="menu-text">Deposit</span>
</button>
<button class="menu-btn" onclick="showView('transferView')">
<!-- <span class="menu-icon">↔️</span> -->
<span class="menu-text">Transfer</span>
</button>
<button class="menu-btn" onclick="showView('balanceView')">
<!-- <span class="menu-icon">📊</span> -->
<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>
<!-- Deposit Screen -->
<div class="atm-view" id="depositView" style="display: none;">
<h3>Deposit Cash</h3>
<div class="deposit-display">
<div class="deposit-info">
<p>Available Cash: <span id="availableCash">$2,500</span></p>
</div>
<div class="custom-amount">
<label>Amount to Deposit</label>
<input type="number" class="amount-input" id="depositInput" placeholder="0.00" min="0"
step="1">
</div>
</div>
<div class="atm-btn-group">
<button class="atm-btn atm-btn-primary" onclick="depositAmount()">
Deposit
</button>
<button class="atm-btn atm-btn-full" onclick="depositAll()">
Deposit All Cash
</button>
<button class="atm-btn atm-btn-secondary" onclick="showView('menuView')">
Back
</button>
</div>
</div>
<!-- Transfer Screen -->
<div class="atm-view" id="transferView" style="display: none;">
<h3>Transfer Funds</h3>
<div class="transfer-display">
<div class="transfer-form">
<div class="form-field">
<label>To Player ID</label>
<input type="text" class="text-input" id="transferPlayerId"
placeholder="Enter player ID">
</div>
<div class="form-field">
<label>Amount</label>
<input type="number" class="amount-input" id="transferAmount" placeholder="0.00" min="0"
step="1">
</div>
</div>
</div>
<div class="atm-btn-group">
<button class="atm-btn atm-btn-primary" onclick="transferFunds()">
Transfer
</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="showView('menuView')">
Back
</button>
</div>
</div>
<!-- Footer -->
<div class="atm-footer">
<div class="footer-text">Secure Banking • 24/7 Access</div>
</div>
</div>
</div>
<script src="atm.js"></script>
</body>
</html>

View File

@ -0,0 +1,284 @@
/**
* ATM Interface
* Handles banking transactions with PIN authentication
*/
// Mock data
const mockData = {
cash: 2500,
bank: 45750,
pin: '1234' // For demo purposes
};
// State
let enteredPin = '';
let currentView = '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';
currentView = viewId;
// Update balance displays when showing certain views
if (viewId === 'menuView' || viewId === 'balanceView' || viewId === 'depositView') {
updateBalances();
}
}
}
// PIN Entry
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');
} else {
dot.classList.remove('filled');
}
});
}
function submitPin() {
if (enteredPin.length !== 4) {
showError('Please enter a 4-digit PIN');
return;
}
// In a real implementation, this would validate with the server
if (enteredPin === mockData.pin) {
enteredPin = '';
updatePinDisplay();
showView('menuView');
} else {
showError('Incorrect PIN');
clearPin();
}
}
// Balance Updates
function updateBalances() {
// Update all balance displays
const cashElements = ['cashBalance', 'cashBalanceDetail', 'availableCash'];
const bankElements = ['bankBalance', 'bankBalanceDetail'];
cashElements.forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = `$${mockData.cash.toLocaleString()}`;
});
bankElements.forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = `$${mockData.bank.toLocaleString()}`;
});
const totalEl = document.getElementById('totalBalance');
if (totalEl) {
const total = mockData.cash + mockData.bank;
totalEl.textContent = `$${total.toLocaleString()}`;
}
}
// Withdraw Functions
function withdrawAmount(amount) {
if (amount > mockData.bank) {
showError('Insufficient funds');
return;
}
mockData.bank -= amount;
mockData.cash += amount;
// sendEvent('atm::withdraw', { amount: amount });
showSuccess(`Withdrew $${amount.toLocaleString()}`);
}
function withdrawCustom() {
const input = document.getElementById('withdrawInput');
const amount = parseFloat(input.value);
if (!amount || amount <= 0) {
showError('Please enter a valid amount');
return;
}
if (amount > mockData.bank) {
showError('Insufficient funds');
return;
}
mockData.bank -= amount;
mockData.cash += amount;
// sendEvent('atm::withdraw', { amount: amount });
input.value = '';
showSuccess(`Withdrew $${amount.toLocaleString()}`);
}
// Deposit Functions
function depositAmount() {
const input = document.getElementById('depositInput');
const amount = parseFloat(input.value);
if (!amount || amount <= 0) {
showError('Please enter a valid amount');
return;
}
if (amount > mockData.cash) {
showError('Insufficient cash');
return;
}
mockData.cash -= amount;
mockData.bank += amount;
// sendEvent('atm::deposit', { amount: amount });
input.value = '';
showSuccess(`Deposited $${amount.toLocaleString()}`);
}
function depositAll() {
if (mockData.cash <= 0) {
showError('No cash to deposit');
return;
}
const amount = mockData.cash;
mockData.cash = 0;
mockData.bank += amount;
// sendEvent('atm::deposit', { amount: amount });
showSuccess(`Deposited $${amount.toLocaleString()}`);
}
// Transfer Function
function transferFunds() {
const playerIdInput = document.getElementById('transferPlayerId');
const amountInput = document.getElementById('transferAmount');
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;
}
if (amount > mockData.bank) {
showError('Insufficient funds');
return;
}
mockData.bank -= amount;
// 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');
}
// Exit ATM
function exitATM() {
enteredPin = '';
updatePinDisplay();
sendEvent('atm::exit', {});
showView('welcomeView');
}
// Send event to Arma
function sendEvent(event, data) {
if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify({
event: event,
data: data
}));
} else {
console.log('Event:', event, 'Data:', data);
}
}
// Update ATM data from external source
function updateATMData(data) {
if (data.cash !== undefined) {
mockData.cash = data.cash;
}
if (data.bank !== undefined) {
mockData.bank = data.bank;
}
updateBalances();
}
// Initialize
function initATM() {
console.log('ATM interface initializing...');
// 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();
}
// Expose functions globally
window.showView = showView;
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.exitATM = exitATM;
window.updateATMData = updateATMData;

View File

@ -0,0 +1,237 @@
<!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>
<!-- <link rel="stylesheet" href="style.css" /> -->
<!--
Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API
This approach is used instead of static HTML imports to work with Arma 3's file system
-->
<script>
Promise.all([
A3API.RequestFile(
"forge\\forge_client\\addons\\bank\\ui\\_site\\style.css",
),
A3API.RequestFile(
"forge\\forge_client\\addons\\bank\\ui\\_site\\script.js",
),
]).then(([css, js]) => {
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
const script = document.createElement("script");
script.text = js;
document.head.appendChild(script);
});
</script>
</head>
<body>
<div class="bank-container">
<!-- Header Section -->
<div class="bank-header">
<div class="bank-logo">
<!-- <div class="logo-icon">💳</div> -->
</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 active" data-account="cash">
<div class="account-header">
<!-- <span class="account-icon">💵</span> -->
<div class="account-info">
<span class="account-name">Cash</span>
<span class="account-type">Physical Currency</span>
</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" data-account="bank">
<div class="account-header">
<!-- <span class="account-icon">🏦</span> -->
<div class="account-info">
<span class="account-name">Bank Account</span>
<span class="account-type">Savings • Protected</span>
</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" data-account="org">
<div class="account-header">
<span class="account-icon">🏢</span>
<div class="account-info">
<span class="account-name">Organization</span>
<span class="account-type">Shared • View Only</span>
</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="cash">Cash ($2,500)</option>
<option value="bank" selected>Bank Account ($45,750)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">To</label>
<select class="form-select" id="transferTo">
<option value="cash">Cash</option>
<option value="bank">Bank Account</option>
<option value="player">Player</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Amount</label>
<input type="number" class="form-input" id="transferAmount" placeholder="0.00" min="0"
step="0.01">
</div>
<div class="form-group" id="playerIdGroup" style="display: none;">
<label class="form-label">Player ID</label>
<input type="text" class="form-input" id="playerId" placeholder="Enter player ID">
</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">
<!-- <span class="quick-action-icon">⬇️</span> -->
<span class="quick-action-label">Deposit All Cash</span>
</button>
<button class="quick-action-btn" data-action="withdraw">
<!-- <span class="quick-action-icon">⬆️</span> -->
<span class="quick-action-label">Withdraw</span>
</button>
<button class="quick-action-btn" id="transferBtn">
<!-- <span class="quick-action-icon">➡️</span> -->
<span class="quick-action-label">Transfer Funds</span>
</button>
<button class="quick-action-btn" data-action="statement">
<!-- <span class="quick-action-icon">📄</span> -->
<span class="quick-action-label">View Statement</span>
</button>
</div>
</div>
</div>
</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 class="transaction-item">
<div class="transaction-header">
<span class="transaction-type deposit">Deposit</span>
<span class="transaction-amount positive">+$5,000</span>
</div>
<div class="transaction-details">
<span class="transaction-desc">From Cash</span>
<span class="transaction-time">2 hours ago</span>
</div>
</div>
<div class="transaction-item">
<div class="transaction-header">
<span class="transaction-type withdrawal">Withdrawal</span>
<span class="transaction-amount negative">-$1,200</span>
</div>
<div class="transaction-details">
<span class="transaction-desc">To Cash</span>
<span class="transaction-time">5 hours ago</span>
</div>
</div>
<div class="transaction-item">
<div class="transaction-header">
<span class="transaction-type transfer">Transfer</span>
<span class="transaction-amount negative">-$500</span>
</div>
<div class="transaction-details">
<span class="transaction-desc">To Player #1234</span>
<span class="transaction-time">1 day ago</span>
</div>
</div>
<div class="transaction-item">
<div class="transaction-header">
<span class="transaction-type deposit">Deposit</span>
<span class="transaction-amount positive">+$10,000</span>
</div>
<div class="transaction-details">
<span class="transaction-desc">Mission Reward</span>
<span class="transaction-time">2 days ago</span>
</div>
</div>
<div class="transaction-item">
<div class="transaction-header">
<span class="transaction-type transfer">Transfer</span>
<span class="transaction-amount positive">+$2,000</span>
</div>
<div class="transaction-details">
<span class="transaction-desc">From Player #5678</span>
<span class="transaction-time">3 days ago</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- <script src="script.js"></script> -->
</body>
</html>

View File

@ -0,0 +1,248 @@
/**
* Banking Interface
* Handles transfers, deposits, withdrawals, and account management
*/
// Mock data
const mockData = {
accounts: {
cash: { name: "Cash", balance: 2500, type: "Physical Currency" },
bank: { name: "Bank Account", balance: 45750, type: "Savings • Protected" },
org: { name: "Organization", balance: 125000, type: "Shared • View Only", readOnly: true }
},
transactions: [
{ type: "deposit", amount: 5000, desc: "From Cash", time: "2 hours ago" },
{ type: "withdrawal", amount: -1200, desc: "To Cash", time: "5 hours ago" },
{ type: "transfer", amount: -500, desc: "To Player #1234", time: "1 day ago" },
{ type: "deposit", amount: 10000, desc: "Mission Reward", time: "2 days ago" },
{ type: "transfer", amount: 2000, desc: "From Player #5678", time: "3 days ago" }
]
};
// State
let selectedAccount = 'cash';
// Initialize
function initBank() {
console.log('Banking interface initializing...');
setupEventHandlers();
updateBalances();
console.log('Banking interface initialized');
}
// Event Handlers
function setupEventHandlers() {
// Close button
const closeBtn = document.querySelector('.close-btn');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
console.log('Closing bank...');
sendEvent('bank::close', {});
});
}
// Account card selection
const accountCards = document.querySelectorAll('.account-card');
accountCards.forEach(card => {
card.addEventListener('click', () => {
accountCards.forEach(c => c.classList.remove('active'));
card.classList.add('active');
selectedAccount = card.dataset.account;
console.log('Selected account:', selectedAccount);
});
});
// Transfer form
const transferBtn = document.getElementById('transferBtn');
const transferFrom = document.getElementById('transferFrom');
const transferTo = document.getElementById('transferTo');
const transferAmount = document.getElementById('transferAmount');
const playerId = document.getElementById('playerId');
const playerIdGroup = document.getElementById('playerIdGroup');
// Show/hide player ID field
if (transferTo) {
transferTo.addEventListener('change', () => {
if (transferTo.value === 'player') {
playerIdGroup.style.display = 'flex';
} else {
playerIdGroup.style.display = 'none';
}
});
}
// Transfer button
if (transferBtn) {
transferBtn.addEventListener('click', () => {
const from = transferFrom.value;
const to = transferTo.value;
const amount = parseFloat(transferAmount.value);
if (!amount || amount <= 0) {
alert('Please enter a valid amount');
return;
}
if (from === to) {
alert('Source and destination must be different');
return;
}
const fromAccount = mockData.accounts[from];
if (amount > fromAccount.balance) {
alert('Insufficient funds');
return;
}
if (to === 'player' && !playerId.value) {
alert('Please enter a player ID');
return;
}
const transferData = {
from: from,
to: to,
amount: amount,
playerId: to === 'player' ? playerId.value : null
};
console.log('Transfer request:', transferData);
sendEvent('bank::transfer', transferData);
// Clear form
transferAmount.value = '';
if (to === 'player') playerId.value = '';
});
}
// Quick action buttons
const quickActionBtns = document.querySelectorAll('.quick-action-btn');
quickActionBtns.forEach(btn => {
btn.addEventListener('click', () => {
const action = btn.dataset.action;
console.log('Quick action:', action);
switch (action) {
case 'deposit':
const cashBalance = mockData.accounts.cash.balance;
if (cashBalance <= 0) {
alert('No cash to deposit');
return;
}
sendEvent('bank::deposit', { amount: cashBalance });
break;
case 'withdraw':
const amount = prompt('Enter amount to withdraw:');
if (amount && parseFloat(amount) > 0) {
sendEvent('bank::withdraw', { amount: parseFloat(amount) });
}
break;
case 'statement':
sendEvent('bank::statement', {});
break;
}
});
});
// Transaction items
const transactionItems = document.querySelectorAll('.transaction-item');
transactionItems.forEach((item, index) => {
item.addEventListener('click', () => {
console.log('Transaction clicked:', mockData.transactions[index]);
sendEvent('bank::transaction::view', { transaction: mockData.transactions[index] });
});
});
}
// Update balances
function updateBalances() {
const balanceElements = document.querySelectorAll('.balance-amount');
balanceElements[0].textContent = `$${mockData.accounts.cash.balance.toLocaleString()}`;
balanceElements[1].textContent = `$${mockData.accounts.bank.balance.toLocaleString()}`;
balanceElements[2].textContent = `$${mockData.accounts.org.balance.toLocaleString()}`;
// Update form options
const transferFrom = document.getElementById('transferFrom');
const transferTo = document.getElementById('transferTo');
if (transferFrom) {
transferFrom.innerHTML = `
<option value="cash">Cash ($${mockData.accounts.cash.balance.toLocaleString()})</option>
<option value="bank" selected>Bank Account ($${mockData.accounts.bank.balance.toLocaleString()})</option>
`;
}
}
// Update bank data
function updateBankData(data) {
if (data.accounts) {
Object.assign(mockData.accounts, data.accounts);
updateBalances();
}
if (data.transactions) {
// Update transaction list
mockData.transactions = data.transactions;
// Re-render transaction list
renderTransactions();
}
}
// Render transactions
function renderTransactions() {
const transactionList = document.querySelector('.transaction-list');
if (!transactionList) return;
transactionList.innerHTML = '';
mockData.transactions.forEach((transaction, index) => {
const item = document.createElement('div');
item.className = 'transaction-item';
const isPositive = transaction.amount > 0;
const amountClass = isPositive ? 'positive' : 'negative';
const displayAmount = isPositive ? `+$${transaction.amount.toLocaleString()}` : `-$${Math.abs(transaction.amount).toLocaleString()}`;
item.innerHTML = `
<div class="transaction-header">
<span class="transaction-type ${transaction.type}">${transaction.type}</span>
<span class="transaction-amount ${amountClass}">${displayAmount}</span>
</div>
<div class="transaction-details">
<span class="transaction-desc">${transaction.desc}</span>
<span class="transaction-time">${transaction.time}</span>
</div>
`;
item.addEventListener('click', () => {
console.log('Transaction clicked:', transaction);
sendEvent('bank::transaction::view', { transaction: transaction });
});
transactionList.appendChild(item);
});
}
// Send event to Arma
function sendEvent(event, data) {
if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify({
event: event,
data: data
}));
} else {
console.log('Event:', event, 'Data:', data);
}
}
// Auto-initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initBank);
} else {
initBank();
}
// Expose functions globally
window.updateBankData = updateBankData;

View File

@ -0,0 +1,471 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 100vh;
width: 100vw;
background: rgba(0, 0, 0, 0.7);
font-family: Arial, sans-serif;
color: rgba(200, 220, 240, 0.95);
overflow: hidden;
}
.bank-container {
height: 100vh;
width: 100vw;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Header Section */
.bank-header {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 1.25rem 1.5rem;
background: rgba(15, 20, 30, 0.9);
border: 1px solid rgba(100, 150, 200, 0.4);
border-radius: 4px;
box-shadow:
0 0 20px rgba(100, 150, 200, 0.15),
0 4px 16px rgba(0, 0, 0, 0.8);
}
.bank-logo {
width: 60px;
height: 60px;
background: rgba(20, 30, 45, 0.8);
border: 2px solid rgba(100, 150, 200, 0.5);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.logo-icon {
font-size: 2rem;
}
.bank-info {
flex: 1;
}
.bank-title {
font-size: 1.5rem;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
color: rgba(200, 220, 255, 1);
margin-bottom: 0.25rem;
}
.bank-subtitle {
font-size: 0.875rem;
color: rgba(140, 160, 180, 0.8);
letter-spacing: 0.5px;
}
.header-actions {
display: flex;
gap: 0.75rem;
}
.action-btn {
padding: 0.625rem 1.25rem;
background: rgba(20, 30, 45, 0.7);
border: 1px solid rgba(100, 150, 200, 0.4);
border-radius: 4px;
color: rgba(200, 220, 240, 0.95);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
transition: all 0.15s ease;
}
.action-btn:hover {
background: rgba(30, 45, 70, 0.9);
border-color: rgba(150, 200, 255, 0.7);
box-shadow:
0 0 15px rgba(100, 150, 200, 0.2),
inset 0 0 20px rgba(100, 150, 200, 0.05);
}
.action-btn-primary {
background: rgba(100, 150, 200, 0.2);
border-color: rgba(100, 150, 200, 0.5);
width: 100%;
margin-top: 0.5rem;
}
.action-btn-primary:hover {
background: rgba(100, 150, 200, 0.3);
border-color: rgba(150, 200, 255, 0.7);
}
.close-btn {
border-color: rgba(200, 100, 100, 0.4);
}
.close-btn:hover {
border-color: rgba(255, 100, 100, 0.7);
box-shadow:
0 0 15px rgba(200, 100, 100, 0.2),
inset 0 0 20px rgba(200, 100, 100, 0.05);
}
/* Main Content */
.bank-content {
flex: 1;
display: grid;
grid-template-columns: 300px 1fr 350px;
gap: 1.5rem;
overflow: hidden;
}
/* Panels */
.bank-panel {
background: rgba(15, 20, 30, 0.9);
border: 1px solid rgba(100, 150, 200, 0.4);
border-left: 3px solid rgba(100, 150, 200, 0.5);
border-radius: 4px;
display: flex;
flex-direction: column;
box-shadow:
0 0 20px rgba(100, 150, 200, 0.1),
0 4px 16px rgba(0, 0, 0, 0.6);
}
.panel-main {
grid-column: 2;
}
.panel-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid rgba(100, 150, 200, 0.2);
}
.panel-title {
font-size: 1rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(200, 220, 255, 1);
}
.panel-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
}
/* Custom Scrollbar */
.panel-content::-webkit-scrollbar {
width: 8px;
}
.panel-content::-webkit-scrollbar-track {
background: rgba(15, 20, 30, 0.5);
border-radius: 4px;
}
.panel-content::-webkit-scrollbar-thumb {
background: rgba(100, 150, 200, 0.3);
border-radius: 4px;
}
.panel-content::-webkit-scrollbar-thumb:hover {
background: rgba(100, 150, 200, 0.5);
}
/* Account Cards */
.account-card {
padding: 1.25rem;
margin-bottom: 1rem;
background: rgba(20, 30, 45, 0.6);
border: 1px solid rgba(100, 150, 200, 0.3);
border-left: 3px solid rgba(100, 150, 200, 0.4);
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
}
.account-card:last-child {
margin-bottom: 0;
}
.account-card:hover {
background: rgba(30, 45, 70, 0.7);
border-left-color: rgba(150, 200, 255, 0.7);
box-shadow:
0 0 15px rgba(100, 150, 200, 0.15),
inset 0 0 20px rgba(100, 150, 200, 0.05);
}
.account-card.active {
background: rgba(30, 45, 70, 0.8);
border-left-color: rgba(100, 200, 150, 0.8);
box-shadow:
0 0 20px rgba(100, 200, 150, 0.2),
inset 0 0 25px rgba(100, 200, 150, 0.05);
}
.account-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.account-icon {
font-size: 1.75rem;
}
.account-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.account-name {
font-size: 1rem;
font-weight: 600;
color: rgba(200, 220, 255, 1);
}
.account-type {
font-size: 0.75rem;
color: rgba(140, 160, 180, 0.8);
}
.account-balance {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid rgba(100, 150, 200, 0.2);
}
.balance-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(140, 160, 180, 0.8);
}
.balance-amount {
font-size: 1.25rem;
font-weight: 600;
color: rgba(100, 200, 150, 1);
}
/* Action Section */
.action-section {
margin-bottom: 2rem;
}
.action-section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(180, 200, 220, 0.9);
margin-bottom: 1rem;
}
/* Transfer Form */
.transfer-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(140, 160, 180, 0.9);
}
.form-select,
.form-input {
padding: 0.75rem 1rem;
background: rgba(20, 30, 45, 0.7);
border: 1px solid rgba(100, 150, 200, 0.3);
border-radius: 4px;
color: rgba(200, 220, 240, 0.95);
font-size: 0.875rem;
transition: all 0.15s ease;
}
.form-select:focus,
.form-input:focus {
outline: none;
border-color: rgba(150, 200, 255, 0.6);
box-shadow:
0 0 15px rgba(100, 150, 200, 0.15),
inset 0 0 20px rgba(100, 150, 200, 0.05);
}
.form-input::placeholder {
color: rgba(100, 120, 140, 0.6);
}
/* Quick Actions */
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 1rem;
}
.quick-action-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
padding: 1.25rem;
background: rgba(20, 30, 45, 0.6);
border: 1px solid rgba(100, 150, 200, 0.3);
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
}
.quick-action-btn:hover {
background: rgba(30, 45, 70, 0.8);
border-color: rgba(150, 200, 255, 0.5);
box-shadow:
0 0 15px rgba(100, 150, 200, 0.15),
inset 0 0 20px rgba(100, 150, 200, 0.05);
}
.quick-action-icon {
font-size: 2rem;
}
.quick-action-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
text-align: center;
color: rgba(180, 200, 220, 0.9);
}
/* Transaction List */
.transaction-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.transaction-item {
padding: 1rem;
background: rgba(20, 30, 45, 0.6);
border: 1px solid rgba(100, 150, 200, 0.2);
border-left: 3px solid rgba(100, 150, 200, 0.4);
border-radius: 4px;
transition: all 0.15s ease;
}
.transaction-item:hover {
background: rgba(30, 45, 70, 0.7);
border-left-color: rgba(150, 200, 255, 0.6);
}
.transaction-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.transaction-type {
padding: 0.25rem 0.625rem;
border-radius: 3px;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.transaction-type.deposit {
background: rgba(100, 200, 150, 0.2);
border: 1px solid rgba(100, 200, 150, 0.4);
color: rgba(150, 255, 200, 0.9);
}
.transaction-type.withdrawal {
background: rgba(200, 150, 100, 0.2);
border: 1px solid rgba(200, 150, 100, 0.4);
color: rgba(255, 200, 150, 0.9);
}
.transaction-type.transfer {
background: rgba(100, 150, 200, 0.2);
border: 1px solid rgba(100, 150, 200, 0.4);
color: rgba(150, 200, 255, 0.9);
}
.transaction-amount {
font-size: 1rem;
font-weight: 600;
}
.transaction-amount.positive {
color: rgba(100, 200, 150, 1);
}
.transaction-amount.negative {
color: rgba(200, 150, 100, 1);
}
.transaction-details {
display: flex;
justify-content: space-between;
align-items: center;
}
.transaction-desc {
font-size: 0.875rem;
color: rgba(180, 200, 220, 0.9);
}
.transaction-time {
font-size: 0.7rem;
color: rgba(100, 150, 200, 0.7);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Responsive adjustments */
@media (max-width: 1400px) {
.bank-content {
grid-template-columns: 280px 1fr 300px;
}
}
@media (max-width: 1200px) {
.bank-content {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto;
}
.panel-main {
grid-column: 1;
}
}

View File

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

View File

@ -0,0 +1,11 @@
class Extended_PreStart_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
};
};
class Extended_PreInit_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
};
};

View File

@ -0,0 +1,4 @@
forge_client_common
===================
Common functionality shared between addons.

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,10 @@
#include "script_component.hpp"
PREP_RECOMPILE_START;
#include "XEH_PREP.hpp"
PREP_RECOMPILE_END;
// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
// #include "initSettings.inc.sqf"
// #include "initKeybinds.inc.sqf"

View File

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

View File

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

View File

@ -0,0 +1,9 @@
#define COMPONENT common
#define COMPONENT_BEAUTIFIED Common
#include "\forge\forge_client\addons\main\script_mod.hpp"
// #define DEBUG_MODE_FULL
// #define DISABLE_COMPILE_CACHE
// #define ENABLE_PERFORMANCE_COUNTERS
#include "\forge\forge_client\addons\main\script_macros.hpp"

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project name="FFE">
<Package name="Common">
<Key ID="STR_forge_client_common_displayName">
<English>Common</English>
</Key>
</Package>
</Project>

View File

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

View File

@ -0,0 +1,13 @@
class CfgSettings {
class CBA {
class Versioning {
class PREFIX {
main_addon = QUOTE(ADDON);
class dependencies {
CBA[] = {"cba_main", REQUIRED_CBA_VERSION, "true"};
};
};
};
};
};

View File

@ -0,0 +1,4 @@
forge_client_main
===================
Main Addon for forge-client

View File

@ -0,0 +1,19 @@
#include "script_component.hpp"
class CfgPatches {
class ADDON {
author = AUTHOR;
authors[] = {"J.Schmidt"};
url = CSTRING(url);
name = COMPONENT_NAME;
requiredVersion = REQUIRED_VERSION;
requiredAddons[] = {
"cba_main"
};
units[] = {};
weapons[] = {};
VERSION_CONFIG;
};
};
#include "CfgSettings.hpp"

Some files were not shown because too many files have changed in this diff Show More