Initial commit
This commit is contained in:
commit
7ce6c0bcad
12
.editorconfig
Normal file
12
.editorconfig
Normal 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
13
.gitattributes
vendored
Normal 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
12
.gitea/CONTRIBUTING.md
Normal 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).
|
||||
25
.gitea/ISSUE_TEMPLATE/bug-report.md
Normal file
25
.gitea/ISSUE_TEMPLATE/bug-report.md
Normal 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.
|
||||
15
.gitea/ISSUE_TEMPLATE/feature-request.md
Normal file
15
.gitea/ISSUE_TEMPLATE/feature-request.md
Normal 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.
|
||||
12
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
12
.gitea/PULL_REQUEST_TEMPLATE.md
Normal 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
|
||||
1
.gitea/assets/placeholder.txt
Normal file
1
.gitea/assets/placeholder.txt
Normal file
@ -0,0 +1 @@
|
||||
|
||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal 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
131
Architecture_Diagram.md
Normal 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
18
Cargo.toml
Normal 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
119
LICENSE.md
Normal file
@ -0,0 +1,119 @@
|
||||

|
||||
|
||||
## 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 Adapter’s 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 Adapter’s 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
294
README.md
Normal 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
12
arma/client/.editorconfig
Normal 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
13
arma/client/.gitattributes
vendored
Normal 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
12
arma/client/.github/CONTRIBUTING.md
vendored
Normal 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).
|
||||
25
arma/client/.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
25
arma/client/.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal 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.
|
||||
15
arma/client/.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
15
arma/client/.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal 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.
|
||||
12
arma/client/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
12
arma/client/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
|
||||
1
arma/client/.github/assets/placeholder.txt
vendored
Normal file
1
arma/client/.github/assets/placeholder.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
|
||||
28
arma/client/.github/workflows/check.yml
vendored
Normal file
28
arma/client/.github/workflows/check.yml
vendored
Normal 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
17
arma/client/.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
# HEMTT
|
||||
hemtt.exe
|
||||
.hemtt/missions/~*
|
||||
.hemttout/
|
||||
releases/
|
||||
|
||||
# Textures
|
||||
Exports/
|
||||
*.spp
|
||||
*.spp.painter_lock
|
||||
*.psd
|
||||
|
||||
# Other
|
||||
*.biprivatekey
|
||||
*.zip
|
||||
*.pbo
|
||||
*.sqfc
|
||||
28
arma/client/.hemtt/commands/ctrlWebBrowserAction.yml
Normal file
28
arma/client/.hemtt/commands/ctrlWebBrowserAction.yml
Normal 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>
|
||||
13
arma/client/.hemtt/hooks/post_release/01_move_readme.rhai
Normal file
13
arma/client/.hemtt/hooks/post_release/01_move_readme.rhai
Normal 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());
|
||||
26
arma/client/.hemtt/hooks/pre_build/01_set_version.rhai
Normal file
26
arma/client/.hemtt/hooks/pre_build/01_set_version.rhai
Normal 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());
|
||||
16
arma/client/.hemtt/launch.toml
Normal file
16
arma/client/.hemtt/launch.toml
Normal 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
|
||||
]
|
||||
40
arma/client/.hemtt/lints.toml
Normal file
40
arma/client/.hemtt/lints.toml
Normal 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
|
||||
24
arma/client/.hemtt/project.toml
Normal file
24
arma/client/.hemtt/project.toml
Normal 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"
|
||||
19
arma/client/.hemtt/scripts/update_build.rhai
Normal file
19
arma/client/.hemtt/scripts/update_build.rhai
Normal 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);
|
||||
23
arma/client/.hemtt/scripts/update_minor.rhai
Normal file
23
arma/client/.hemtt/scripts/update_minor.rhai
Normal 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);
|
||||
20
arma/client/.hemtt/scripts/update_patch.rhai
Normal file
20
arma/client/.hemtt/scripts/update_patch.rhai
Normal 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
119
arma/client/LICENSE.md
Normal file
@ -0,0 +1,119 @@
|
||||

|
||||
|
||||
## 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 Adapter’s 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 Adapter’s 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
27
arma/client/README.md
Normal 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).
|
||||
1
arma/client/addons/actor/$PBOPREFIX$
Normal file
1
arma/client/addons/actor/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
||||
forge\forge_client\addons\actor
|
||||
19
arma/client/addons/actor/CfgEventHandlers.hpp
Normal file
19
arma/client/addons/actor/CfgEventHandlers.hpp
Normal 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));
|
||||
};
|
||||
};
|
||||
4
arma/client/addons/actor/README.md
Normal file
4
arma/client/addons/actor/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
forge_client_actor
|
||||
===================
|
||||
|
||||
Description for this addon
|
||||
3
arma/client/addons/actor/XEH_PREP.hpp
Normal file
3
arma/client/addons/actor/XEH_PREP.hpp
Normal file
@ -0,0 +1,3 @@
|
||||
PREP(handleUIEvents);
|
||||
PREP(initActorClass);
|
||||
PREP(openUI);
|
||||
1
arma/client/addons/actor/XEH_postInit.sqf
Normal file
1
arma/client/addons/actor/XEH_postInit.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
42
arma/client/addons/actor/XEH_postInitClient.sqf
Normal file
42
arma/client/addons/actor/XEH_postInitClient.sqf
Normal 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);
|
||||
10
arma/client/addons/actor/XEH_preInit.sqf
Normal file
10
arma/client/addons/actor/XEH_preInit.sqf
Normal 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"
|
||||
1
arma/client/addons/actor/XEH_preInitClient.sqf
Normal file
1
arma/client/addons/actor/XEH_preInitClient.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
3
arma/client/addons/actor/XEH_preStart.sqf
Normal file
3
arma/client/addons/actor/XEH_preStart.sqf
Normal file
@ -0,0 +1,3 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
#include "XEH_PREP.hpp"
|
||||
21
arma/client/addons/actor/config.cpp
Normal file
21
arma/client/addons/actor/config.cpp
Normal 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"
|
||||
45
arma/client/addons/actor/functions/fnc_handleUIEvents.sqf
Normal file
45
arma/client/addons/actor/functions/fnc_handleUIEvents.sqf
Normal 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;
|
||||
165
arma/client/addons/actor/functions/fnc_initActorClass.sqf
Normal file
165
arma/client/addons/actor/functions/fnc_initActorClass.sqf
Normal 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)
|
||||
31
arma/client/addons/actor/functions/fnc_openUI.sqf
Normal file
31
arma/client/addons/actor/functions/fnc_openUI.sqf
Normal 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;
|
||||
8
arma/client/addons/actor/initKeybinds.inc.sqf
Normal file
8
arma/client/addons/actor/initKeybinds.inc.sqf
Normal 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;
|
||||
24
arma/client/addons/actor/initSettings.inc.sqf
Normal file
24
arma/client/addons/actor/initSettings.inc.sqf
Normal 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;
|
||||
9
arma/client/addons/actor/script_component.hpp
Normal file
9
arma/client/addons/actor/script_component.hpp
Normal 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"
|
||||
38
arma/client/addons/actor/stringtable.xml
Normal file
38
arma/client/addons/actor/stringtable.xml
Normal 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>
|
||||
21
arma/client/addons/actor/ui/RscActorMenu.hpp
Normal file
21
arma/client/addons/actor/ui/RscActorMenu.hpp
Normal 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};
|
||||
};
|
||||
};
|
||||
};
|
||||
98
arma/client/addons/actor/ui/RscCommon.hpp
Normal file
98
arma/client/addons/actor/ui/RscCommon.hpp
Normal 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;
|
||||
605
arma/client/addons/actor/ui/_site/garage.css
Normal file
605
arma/client/addons/actor/ui/_site/garage.css
Normal 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;
|
||||
}
|
||||
}
|
||||
205
arma/client/addons/actor/ui/_site/garage.html
Normal file
205
arma/client/addons/actor/ui/_site/garage.html
Normal 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>
|
||||
317
arma/client/addons/actor/ui/_site/garage.js
Normal file
317
arma/client/addons/actor/ui/_site/garage.js
Normal 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;
|
||||
70
arma/client/addons/actor/ui/_site/index.html
Normal file
70
arma/client/addons/actor/ui/_site/index.html
Normal 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>
|
||||
424
arma/client/addons/actor/ui/_site/script.js
Normal file
424
arma/client/addons/actor/ui/_site/script.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
417
arma/client/addons/actor/ui/_site/script.js.bak
Normal file
417
arma/client/addons/actor/ui/_site/script.js.bak
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
567
arma/client/addons/actor/ui/_site/store.css
Normal file
567
arma/client/addons/actor/ui/_site/store.css
Normal 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;
|
||||
}
|
||||
}
|
||||
149
arma/client/addons/actor/ui/_site/store.html
Normal file
149
arma/client/addons/actor/ui/_site/store.html
Normal 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>
|
||||
339
arma/client/addons/actor/ui/_site/store.js
Normal file
339
arma/client/addons/actor/ui/_site/store.js
Normal 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;
|
||||
116
arma/client/addons/actor/ui/_site/style.css
Normal file
116
arma/client/addons/actor/ui/_site/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
186
arma/client/addons/actor/ui/_site/style.css.bak
Normal file
186
arma/client/addons/actor/ui/_site/style.css.bak
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
arma/client/addons/bank/$PBOPREFIX$
Normal file
1
arma/client/addons/bank/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
||||
forge\forge_client\addons\bank
|
||||
19
arma/client/addons/bank/CfgEventHandlers.hpp
Normal file
19
arma/client/addons/bank/CfgEventHandlers.hpp
Normal 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));
|
||||
};
|
||||
};
|
||||
4
arma/client/addons/bank/README.md
Normal file
4
arma/client/addons/bank/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
forge_client_bank
|
||||
===================
|
||||
|
||||
Description for this addon
|
||||
3
arma/client/addons/bank/XEH_PREP.hpp
Normal file
3
arma/client/addons/bank/XEH_PREP.hpp
Normal file
@ -0,0 +1,3 @@
|
||||
PREP(handleUIEvents);
|
||||
PREP(initBankClass);
|
||||
PREP(openUI);
|
||||
1
arma/client/addons/bank/XEH_postInit.sqf
Normal file
1
arma/client/addons/bank/XEH_postInit.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
28
arma/client/addons/bank/XEH_postInitClient.sqf
Normal file
28
arma/client/addons/bank/XEH_postInitClient.sqf
Normal 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);
|
||||
10
arma/client/addons/bank/XEH_preInit.sqf
Normal file
10
arma/client/addons/bank/XEH_preInit.sqf
Normal 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"
|
||||
1
arma/client/addons/bank/XEH_preInitClient.sqf
Normal file
1
arma/client/addons/bank/XEH_preInitClient.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
3
arma/client/addons/bank/XEH_preStart.sqf
Normal file
3
arma/client/addons/bank/XEH_preStart.sqf
Normal file
@ -0,0 +1,3 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
#include "XEH_PREP.hpp"
|
||||
21
arma/client/addons/bank/config.cpp
Normal file
21
arma/client/addons/bank/config.cpp
Normal 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"
|
||||
33
arma/client/addons/bank/functions/fnc_handleUIEvents.sqf
Normal file
33
arma/client/addons/bank/functions/fnc_handleUIEvents.sqf
Normal 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;
|
||||
88
arma/client/addons/bank/functions/fnc_initBankClass.sqf
Normal file
88
arma/client/addons/bank/functions/fnc_initBankClass.sqf
Normal 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)
|
||||
31
arma/client/addons/bank/functions/fnc_openUI.sqf
Normal file
31
arma/client/addons/bank/functions/fnc_openUI.sqf
Normal 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;
|
||||
1
arma/client/addons/bank/initKeybinds.inc.sqf
Normal file
1
arma/client/addons/bank/initKeybinds.inc.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "\forge\forge_client\addons\main\data\hpp\defineDIKCodes.hpp"
|
||||
1
arma/client/addons/bank/initSettings.inc.sqf
Normal file
1
arma/client/addons/bank/initSettings.inc.sqf
Normal file
@ -0,0 +1 @@
|
||||
// Can use localize "STR_ACE_Common_Enabled" for name if ACE is required
|
||||
9
arma/client/addons/bank/script_component.hpp
Normal file
9
arma/client/addons/bank/script_component.hpp
Normal 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"
|
||||
8
arma/client/addons/bank/stringtable.xml
Normal file
8
arma/client/addons/bank/stringtable.xml
Normal 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>
|
||||
21
arma/client/addons/bank/ui/RscBank.hpp
Normal file
21
arma/client/addons/bank/ui/RscBank.hpp
Normal 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};
|
||||
};
|
||||
};
|
||||
};
|
||||
98
arma/client/addons/bank/ui/RscCommon.hpp
Normal file
98
arma/client/addons/bank/ui/RscCommon.hpp
Normal 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;
|
||||
585
arma/client/addons/bank/ui/_site/atm.css
Normal file
585
arma/client/addons/bank/ui/_site/atm.css
Normal 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;
|
||||
}
|
||||
}
|
||||
255
arma/client/addons/bank/ui/_site/atm.html
Normal file
255
arma/client/addons/bank/ui/_site/atm.html
Normal 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>
|
||||
284
arma/client/addons/bank/ui/_site/atm.js
Normal file
284
arma/client/addons/bank/ui/_site/atm.js
Normal 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;
|
||||
237
arma/client/addons/bank/ui/_site/index.html
Normal file
237
arma/client/addons/bank/ui/_site/index.html
Normal 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>
|
||||
248
arma/client/addons/bank/ui/_site/script.js
Normal file
248
arma/client/addons/bank/ui/_site/script.js
Normal 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;
|
||||
471
arma/client/addons/bank/ui/_site/style.css
Normal file
471
arma/client/addons/bank/ui/_site/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
1
arma/client/addons/common/$PBOPREFIX$
Normal file
1
arma/client/addons/common/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
||||
forge\forge_client\addons\common
|
||||
11
arma/client/addons/common/CfgEventHandlers.hpp
Normal file
11
arma/client/addons/common/CfgEventHandlers.hpp
Normal 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));
|
||||
};
|
||||
};
|
||||
4
arma/client/addons/common/README.md
Normal file
4
arma/client/addons/common/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
forge_client_common
|
||||
===================
|
||||
|
||||
Common functionality shared between addons.
|
||||
1
arma/client/addons/common/XEH_PREP.hpp
Normal file
1
arma/client/addons/common/XEH_PREP.hpp
Normal file
@ -0,0 +1 @@
|
||||
|
||||
10
arma/client/addons/common/XEH_preInit.sqf
Normal file
10
arma/client/addons/common/XEH_preInit.sqf
Normal 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"
|
||||
3
arma/client/addons/common/XEH_preStart.sqf
Normal file
3
arma/client/addons/common/XEH_preStart.sqf
Normal file
@ -0,0 +1,3 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
#include "XEH_PREP.hpp"
|
||||
19
arma/client/addons/common/config.cpp
Normal file
19
arma/client/addons/common/config.cpp
Normal 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"
|
||||
9
arma/client/addons/common/script_component.hpp
Normal file
9
arma/client/addons/common/script_component.hpp
Normal 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"
|
||||
8
arma/client/addons/common/stringtable.xml
Normal file
8
arma/client/addons/common/stringtable.xml
Normal 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>
|
||||
1
arma/client/addons/main/$PBOPREFIX$
Normal file
1
arma/client/addons/main/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
||||
forge\forge_client\addons\main
|
||||
13
arma/client/addons/main/CfgSettings.hpp
Normal file
13
arma/client/addons/main/CfgSettings.hpp
Normal 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"};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
4
arma/client/addons/main/README.md
Normal file
4
arma/client/addons/main/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
forge_client_main
|
||||
===================
|
||||
|
||||
Main Addon for forge-client
|
||||
19
arma/client/addons/main/config.cpp
Normal file
19
arma/client/addons/main/config.cpp
Normal 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
Loading…
x
Reference in New Issue
Block a user