feat: implement initial in-game store system with web-based UI.

This commit is contained in:
Jacob Schmidt 2026-02-06 06:22:32 -06:00
parent 56f0f1c971
commit 58902f0c29
24 changed files with 371 additions and 50 deletions

View File

@ -4,7 +4,7 @@
* File: fnc_handleUIEvents.sqf
* Author: IDSolutions
* Date: 2026-01-28
* Last Update: 2026-02-04
* Last Update: 2026-02-06
* Public: No
*
* Description:
@ -41,7 +41,7 @@ switch (_event) do {
case "actor::open::vlocker": { [FORGE_Locker_Box, player, false] spawn AFUNC(arsenal,openBox) };
case "actor::open::phone": { hint "Phone interaction is not yet implemented."; };
case "actor::open::iplayer": { hint "Player interaction is not yet implemented." };
case "actor::open::store": { hint "Store interaction is not yet implemented."; };
case "actor::open::store": { [] spawn EFUNC(store,openUI); };
default { hint format ["Unhandled UI event: %1", _event]; };
};

View File

@ -75,6 +75,13 @@ const baseMenuItems = [
icon: "",
action: "actor::open::org",
},
{
id: "store",
title: "Store",
description: "Browse and purchase items from the store",
icon: "",
action: "actor::open::store",
},
];
const actionDefinitions = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,38 @@
#include "..\script_component.hpp"
/*
* File: fnc_handleUIEvents.sqf
* Author: IDSolutions
* Date: 2026-01-28
* Last Update: 2026-02-06
* Public: No
*
* Description:
* Handles the UI events.
*
* Arguments:
* 0: [CONTROL] - The control that triggered the event
* 1: [BOOL] - Whether the event is from a confirm dialog
* 2: [STRING] - The message containing the event data
*
* Return Value:
* UI events handled [BOOL]
*
* Example:
* call forge_client_store_fnc_handleUIEvents;
*/
params ["_control", "_isConfirmDialog", "_message"];
private _alert = fromJSON _message;
private _event = _alert get "event";
private _data = _alert get "data";
diag_log format ["[FORGE:Client:Store] Handling UI event: %1 with data: %2", _event, _data];
switch (_event) do {
case "store::close": { closeDialog 1; };
default { hint format ["Unhandled UI event: %1", _event]; };
};
true;

View File

@ -0,0 +1,38 @@
#include "..\script_component.hpp"
/*
* File: fnc_initStoreClass.sqf
* Author: IDSolutions
* Date: 2026-01-28
* Last Update: 2026-02-06
* Public: Yes
*
* Description:
* Initializes the store class for managing store data.
* Provides methods for loading and applying store data.
*
* Arguments:
* None
*
* Return Value:
* Store class object [HASHMAP OBJECT]
*
* Example:
* call forge_client_store_fnc_initStoreClass
*/
#pragma hemtt ignore_variables ["_self"]
GVAR(StoreClass) = createHashMapObject [[
["#type", "IStoreClass"],
["#create", {
_self set ["uid", getPlayerUID player];
_self set ["store", createHashMap];
_self set ["isLoaded", false];
_self set ["lastSave", time];
systemChat format ["Store class initialized for %1", (name player)];
diag_log "[FORGE:Client:Store] Store Class Initialized!";
}]
]];
GVAR(StoreClass)

View File

@ -0,0 +1,35 @@
#include "..\script_component.hpp"
/*
* File: fnc_openUI.sqf
* Author: IDSolutions
* Date: 2026-01-28
* Last Update: 2026-02-06
* Public: No
*
* Description:
* Opens the store interface.
*
* Arguments:
* None
*
* Return Value:
* UI opened [BOOL]
*
* Example:
* call forge_client_store_fnc_openUI;
*/
private _display = createDialog ["RscStore", true];
private _ctrl = _display displayCtrl 1004;
_ctrl ctrlAddEventHandler ["JSDialog", {
params ["_control", "_isConfirmDialog", "_message"];
[_control, _isConfirmDialog, _message] call FUNC(handleUIEvents);
}];
_ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\index.html)];
// _ctrl ctrlWebBrowserAction ["OpenDevConsole"];
true;

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Store</title>
<link rel="stylesheet" href="store.css" />
<!-- <link rel="stylesheet" href="style.css" /> -->
<!--
Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API
@ -14,10 +14,10 @@
<script>
Promise.all([
A3API.RequestFile(
"forge\\forge_client\\addons\\actor\\ui\\_site\\store.css",
"forge\\forge_client\\addons\\store\\ui\\_site\\style.css",
),
A3API.RequestFile(
"forge\\forge_client\\addons\\actor\\ui\\_site\\store.js",
"forge\\forge_client\\addons\\store\\ui\\_site\\script.js",
),
]).then(([css, js]) => {
const style = document.createElement("style");
@ -48,7 +48,7 @@
</div>
<div class="header-actions">
<button class="action-btn cart-btn" id="cartToggle">
<span class="cart-icon">🛒</span>
<span class="cart-icon">Cart</span>
<span class="cart-count">0</span>
</button>
<button class="action-btn close-btn">Close</button>
@ -65,27 +65,27 @@
<div class="panel-content">
<div class="category-list">
<button class="category-item active" data-category="all">
<span class="category-icon">📦</span>
<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-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-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-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-icon"></span>
<span class="category-name">Supplies</span>
<span class="category-count">5</span>
</button>
@ -117,7 +117,7 @@
<div class="panel-content">
<div class="cart-items" id="cartItems">
<div class="empty-cart">
<span class="empty-icon">🛒</span>
<span class="empty-icon"></span>
<span class="empty-text">Your cart is empty</span>
</div>
</div>
@ -143,7 +143,7 @@
</div>
</div>
<script src="store.js"></script>
<!-- <script src="script.js"></script> -->
</body>
</html>

View File

@ -8,36 +8,36 @@ 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 },
{ id: 1, name: "Assault Rifle", category: "weapons", image: "", price: 2500 },
{ id: 2, name: "Sniper Rifle", category: "weapons", image: "", price: 4500 },
{ id: 3, name: "SMG", category: "weapons", image: "", price: 1800 },
{ id: 4, name: "Pistol", category: "weapons", image: "", price: 800 },
{ id: 5, name: "Shotgun", category: "weapons", image: "", price: 1500 },
{ id: 6, name: "LMG", category: "weapons", image: "", price: 3500 },
{ id: 7, name: "Grenade Launcher", category: "weapons", image: "", price: 5000 },
{ id: 8, name: "Rocket Launcher", category: "weapons", image: "", 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 },
{ id: 9, name: "Body Armor", category: "equipment", image: "", price: 3000 },
{ id: 10, name: "Helmet", category: "equipment", image: "", price: 1200 },
{ id: 11, name: "Night Vision", category: "equipment", image: "", price: 2500 },
{ id: 12, name: "GPS Device", category: "equipment", image: "", price: 800 },
{ id: 13, name: "Radio", category: "equipment", image: "", price: 600 },
{ id: 14, name: "Backpack", category: "equipment", image: "", 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 },
{ id: 15, name: "First Aid Kit", category: "medical", image: "", price: 400 },
{ id: 16, name: "Med Kit", category: "medical", image: "", price: 1000 },
{ id: 17, name: "Bandages", category: "medical", image: "", price: 150 },
{ id: 18, name: "Morphine", category: "medical", image: "", price: 300 },
{ id: 19, name: "Blood Bag", category: "medical", image: "", 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 }
{ id: 20, name: "Ammunition Box", category: "supplies", image: "", price: 800 },
{ id: 21, name: "Explosive Charges", category: "supplies", image: "", price: 1500 },
{ id: 22, name: "Toolkit", category: "supplies", image: "", price: 600 },
{ id: 23, name: "Food Rations", category: "supplies", image: "", price: 200 },
{ id: 24, name: "Water Canteen", category: "supplies", image: "", price: 150 }
]
};
@ -64,7 +64,7 @@ function setupEventHandlers() {
if (closeBtn) {
closeBtn.addEventListener('click', () => {
console.log('Closing store...');
sendEvent('actor::close::store', {});
sendEvent('store::close', {});
});
}
@ -136,8 +136,7 @@ function renderItems() {
if (searchQuery) {
filteredItems = filteredItems.filter(item =>
item.name.toLowerCase().includes(searchQuery) ||
item.description.toLowerCase().includes(searchQuery)
item.name.toLowerCase().includes(searchQuery)
);
}
@ -147,9 +146,8 @@ function renderItems() {
card.className = 'item-card';
card.innerHTML = `
<div class="item-icon">${item.icon}</div>
<div class="item-image"><img src="${item.image}" alt="${item.name}"></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>
@ -207,7 +205,7 @@ function renderCart() {
if (cart.length === 0) {
cartItems.innerHTML = `
<div class="empty-cart">
<span class="empty-icon">🛒</span>
<span class="empty-icon"></span>
<span class="empty-text">Your cart is empty</span>
</div>
`;
@ -266,7 +264,7 @@ function handleCheckout() {
const grandTotal = total + tax;
if (grandTotal > mockData.balance) {
alert('Insufficient funds!');
// alert('Insufficient funds!');
return;
}
@ -283,7 +281,7 @@ function handleCheckout() {
};
console.log('Purchase request:', purchaseData);
sendEvent('actor::store::purchase', purchaseData);
sendEvent('store::purchase', purchaseData);
// Clear cart after purchase
cart = [];

View File

@ -128,7 +128,7 @@ body {
}
.cart-icon {
font-size: 1.25rem;
font-size: 1rem;
}
.cart-count {
@ -202,7 +202,6 @@ body {
.panel-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
}
/* Custom Scrollbar */
@ -312,6 +311,8 @@ body {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
overflow-y: auto;
height: 590px;
}
.item-card {
@ -324,7 +325,8 @@ body {
transition: all 0.15s ease;
display: flex;
flex-direction: column;
gap: 0.75rem;
gap: 0.5rem;
height: 300px !important;
}
.item-card:hover {
@ -335,8 +337,8 @@ body {
inset 0 0 20px rgba(100, 150, 200, 0.05);
}
.item-icon {
font-size: 3rem;
.item-image {
height: 256px;
text-align: center;
}