feat(store): enhance product card with tags and metadata
- Added support for displaying product tags in the UI, including side and faction labels. - Updated CSS to style product tags for better visibility. - Modified StoreView and store registry to include side and faction information for items. - Enhanced server-side catalog service to filter items based on the player's side. - Updated documentation to reflect changes in store behavior and metadata handling.
This commit is contained in:
parent
c676a9084e
commit
19eae5acfa
1
.gitignore
vendored
1
.gitignore
vendored
@ -37,4 +37,5 @@ Thumbs.db
|
||||
arma/ui/map-viewer/
|
||||
arma/mod/.hemttout/
|
||||
arma/server/surrealdb/forge.db/
|
||||
arma/temp/
|
||||
promo/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -94,6 +94,7 @@ ${scopeSelector} .product-card:hover {
|
||||
|
||||
${scopeSelector} .card-kicker,
|
||||
${scopeSelector} .product-code,
|
||||
${scopeSelector} .product-tag,
|
||||
${scopeSelector} .empty-state-kicker {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.14em;
|
||||
@ -156,6 +157,21 @@ ${scopeSelector} .product-meta {
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .product-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .product-tag {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
padding: 0.18rem 0.42rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--store-accent-line);
|
||||
background: rgb(255 255 255 / 0.42);
|
||||
}
|
||||
|
||||
${scopeSelector} .product-name {
|
||||
font-size: 0.96rem;
|
||||
font-weight: 700;
|
||||
@ -370,6 +386,10 @@ ${scopeSelector} .product-copy {
|
||||
item.description,
|
||||
item.className || item.code,
|
||||
);
|
||||
const tags = [
|
||||
item.sideLabel ? item.sideLabel : "",
|
||||
item.factionName ? item.factionName : "",
|
||||
].filter(Boolean);
|
||||
|
||||
return h(
|
||||
"article",
|
||||
@ -399,6 +419,15 @@ ${scopeSelector} .product-copy {
|
||||
{ className: "product-code" },
|
||||
item.type || item.code || item.className,
|
||||
),
|
||||
tags.length > 0
|
||||
? h(
|
||||
"div",
|
||||
{ className: "product-tags" },
|
||||
tags.map((tag) =>
|
||||
h("span", { className: "product-tag" }, tag),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
h("strong", { className: "product-name" }, item.name),
|
||||
),
|
||||
h("p", { className: "product-copy" }, description),
|
||||
|
||||
@ -195,6 +195,8 @@
|
||||
item.description,
|
||||
item.price,
|
||||
item.type,
|
||||
item.sideLabel,
|
||||
item.factionName,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@ -17,6 +17,10 @@
|
||||
type: String(item?.type || ""),
|
||||
category: String(item?.category || ""),
|
||||
entryKind: String(item?.entryKind || "item"),
|
||||
side: String(item?.side || ""),
|
||||
sideLabel: String(item?.sideLabel || ""),
|
||||
faction: String(item?.faction || ""),
|
||||
factionName: String(item?.factionName || ""),
|
||||
quantity: Math.max(0, Number(item?.quantity || 0)),
|
||||
};
|
||||
}
|
||||
|
||||
@ -71,10 +71,12 @@ class CfgStore {
|
||||
};
|
||||
```
|
||||
|
||||
`dynamic` keeps the full generated catalog. `allowlist` only shows classnames
|
||||
listed for the requested category. `denylist` removes listed classnames from the
|
||||
generated category. Overrides are applied server-side, so checkout validation
|
||||
uses the same prices and descriptions the UI displays.
|
||||
`modMode` is applied first. After that, any non-empty
|
||||
`Categories.<category>[]` array acts as an explicit allowlist for only that
|
||||
category, regardless of `mode`. Categories with empty arrays follow `mode`:
|
||||
`dynamic` keeps generated entries and `allowlist` hides the category. Overrides
|
||||
are applied server-side, so checkout validation uses the same prices and
|
||||
descriptions the UI displays.
|
||||
|
||||
`modMode` applies before category filtering. `dynamic` means no mod-source
|
||||
filtering. `allowlist` only keeps generated entries that match one of the
|
||||
@ -86,9 +88,10 @@ metadata tokens that can appear anywhere, and `dlcs[]` for DLC/source/author
|
||||
labels used by Creator DLC content. If a mod source defines no patches, it is
|
||||
treated as available and only the source/prefix/contains/DLC checks are used.
|
||||
|
||||
`units[]` follows the same `dynamic`, `allowlist`, and `denylist` behavior as
|
||||
item and vehicle categories. Unit purchases are immediate spawn grants, not
|
||||
durable virtual garage unlocks.
|
||||
Unit catalog responses are additionally filtered to the requesting player's
|
||||
side. Unit entries include side and faction metadata for UI display, and
|
||||
checkout validation rejects unit classnames from another side. Unit purchases
|
||||
are immediate spawn grants, not durable virtual garage unlocks.
|
||||
|
||||
The filter is currently global for the mission. Revisit per-store profile
|
||||
support if individual vendors need different inventories.
|
||||
|
||||
@ -20,7 +20,7 @@ PREP_RECOMPILE_END;
|
||||
diag_log "[FORGE:Server:Store] Store catalog service is unavailable."
|
||||
};
|
||||
|
||||
private _result = GVAR(StoreCatalogService) call ["buildCategoryResponse", [_category]];
|
||||
private _result = GVAR(StoreCatalogService) call ["buildCategoryResponse", [_category, _player]];
|
||||
[CRPC(store,responseCategory), [_result], _player] call CFUNC(targetEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
|
||||
@ -294,21 +294,74 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
private _classNames = _self call ["getMissionStoreCategoryList", [_category]];
|
||||
private _filteredItems = _self call ["applyMissionStoreModFilter", [_items]];
|
||||
|
||||
switch (_mode) do {
|
||||
case "allowlist": {
|
||||
_filteredItems = _filteredItems select {
|
||||
(toLowerANSI (_x getOrDefault ["className", ""])) in _classNames
|
||||
};
|
||||
if (_classNames isNotEqualTo []) then {
|
||||
_filteredItems = _filteredItems select {
|
||||
(toLowerANSI (_x getOrDefault ["className", ""])) in _classNames
|
||||
};
|
||||
case "denylist": {
|
||||
_filteredItems = _filteredItems select {
|
||||
!((toLowerANSI (_x getOrDefault ["className", ""])) in _classNames)
|
||||
} else {
|
||||
switch (_mode) do {
|
||||
case "allowlist": { _filteredItems = []; };
|
||||
case "denylist": {
|
||||
_filteredItems = _filteredItems select {
|
||||
!((toLowerANSI (_x getOrDefault ["className", ""])) in _classNames)
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
_filteredItems apply { _self call ["applyMissionStoreOverrides", [_x]] }
|
||||
}],
|
||||
["resolveSideLabel", compileFinal {
|
||||
params [["_sideValue", -1, [0]]];
|
||||
|
||||
switch _sideValue do {
|
||||
case 0: { "OPFOR" };
|
||||
case 1: { "BLUFOR" };
|
||||
case 2: { "Independent" };
|
||||
case 3: { "Civilian" };
|
||||
default { "Unknown" };
|
||||
}
|
||||
}],
|
||||
["resolveSideKey", compileFinal {
|
||||
params [["_sideValue", -1, [0]]];
|
||||
|
||||
switch _sideValue do {
|
||||
case 0: { "east" };
|
||||
case 1: { "west" };
|
||||
case 2: { "resistance" };
|
||||
case 3: { "civilian" };
|
||||
default { "" };
|
||||
}
|
||||
}],
|
||||
["resolvePlayerSideKey", compileFinal {
|
||||
params [["_player", objNull, [objNull]]];
|
||||
|
||||
if (isNull _player) exitWith { "" };
|
||||
switch (side group _player) do {
|
||||
case west: { "west" };
|
||||
case east: { "east" };
|
||||
case independent: { "resistance" };
|
||||
case civilian: { "civilian" };
|
||||
default { "" };
|
||||
}
|
||||
}],
|
||||
["doesItemMatchPlayerSide", compileFinal {
|
||||
params [["_item", createHashMap, [createHashMap]], ["_player", objNull, [objNull]]];
|
||||
|
||||
if (_item isEqualTo createHashMap) exitWith { false };
|
||||
|
||||
private _playerSideKey = _self call ["resolvePlayerSideKey", [_player]];
|
||||
if (_playerSideKey isEqualTo "") exitWith { true };
|
||||
|
||||
private _itemSideKey = _item getOrDefault ["side", ""];
|
||||
_itemSideKey isEqualTo "" || { _itemSideKey isEqualTo _playerSideKey }
|
||||
}],
|
||||
["applyPlayerSideFilter", compileFinal {
|
||||
params [["_category", "", [""]], ["_items", [], [[]]], ["_player", objNull, [objNull]]];
|
||||
|
||||
if !(_self call ["isUnitCategory", [_category]]) exitWith { +_items };
|
||||
_items select { _self call ["doesItemMatchPlayerSide", [_x, _player]] }
|
||||
}],
|
||||
["formatCurrency", compileFinal {
|
||||
params [["_amount", 0, [0]]];
|
||||
|
||||
@ -384,8 +437,7 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
};
|
||||
|
||||
private _priceValue = _self call ["calculateCatalogPriceValue", [_cfg, _isVehicle]];
|
||||
|
||||
createHashMapFromArray [
|
||||
private _item = createHashMapFromArray [
|
||||
["className", _className],
|
||||
["code", _className],
|
||||
["name", _displayName],
|
||||
@ -398,7 +450,26 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
["sourceMod", _sourceMod],
|
||||
["sourceDLC", _sourceDLC],
|
||||
["sourceAuthor", _sourceAuthor]
|
||||
]
|
||||
];
|
||||
|
||||
if (isNumber (_cfg >> "side")) then {
|
||||
private _sideValue = getNumber (_cfg >> "side");
|
||||
_item set ["sideValue", _sideValue];
|
||||
_item set ["side", _self call ["resolveSideKey", [_sideValue]]];
|
||||
_item set ["sideLabel", _self call ["resolveSideLabel", [_sideValue]]];
|
||||
};
|
||||
|
||||
if (isText (_cfg >> "faction")) then {
|
||||
private _faction = getText (_cfg >> "faction");
|
||||
if (_faction isNotEqualTo "") then {
|
||||
private _factionName = getText (configFile >> "CfgFactionClasses" >> _faction >> "displayName");
|
||||
if (_factionName isEqualTo "") then { _factionName = _faction; };
|
||||
_item set ["faction", _faction];
|
||||
_item set ["factionName", _factionName];
|
||||
};
|
||||
};
|
||||
|
||||
_item
|
||||
}],
|
||||
["appendCfgWeaponsByItemInfoType", compileFinal {
|
||||
params [["_items", [], [[]]], ["_itemInfoType", -1, [0]], ["_itemKind", "", [""]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]];
|
||||
@ -686,7 +757,7 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
_items
|
||||
}],
|
||||
["buildCategoryResponse", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
params [["_category", "", [""]], ["_player", objNull, [objNull]]];
|
||||
|
||||
private _categoryKey = _self call ["normalizeCategoryKey", [_category]];
|
||||
private _response = createHashMapFromArray [["success", false], ["category", _categoryKey], ["items", []], ["message", "No store category was provided."]];
|
||||
@ -699,7 +770,10 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
|
||||
_response set ["success", true];
|
||||
_response set ["message", ""];
|
||||
_response set ["items", _self call ["buildCategoryItems", [_categoryKey]]];
|
||||
|
||||
private _items = _self call ["buildCategoryItems", [_categoryKey]];
|
||||
_items = _self call ["applyPlayerSideFilter", [_categoryKey, _items, _player]];
|
||||
_response set ["items", _items];
|
||||
_response
|
||||
}],
|
||||
["resolveCheckoutCategories", compileFinal {
|
||||
@ -734,7 +808,7 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
_resolved
|
||||
}],
|
||||
["buildCheckoutRequest", compileFinal {
|
||||
params [["_items", [], [[]]], ["_vehicles", [], [[]]], ["_units", [], [[]]]];
|
||||
params [["_items", [], [[]]], ["_vehicles", [], [[]]], ["_units", [], [[]]], ["_player", objNull, [objNull]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
@ -810,6 +884,12 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
if (_catalogEntry isEqualTo createHashMap) then {
|
||||
_message = format ["Unsupported store unit: %1", _className];
|
||||
} else {
|
||||
if !(_self call ["doesItemMatchPlayerSide", [_catalogEntry, _player]]) then {
|
||||
_message = format ["Store unit is not available for your side: %1", _className];
|
||||
};
|
||||
};
|
||||
|
||||
if (_message isEqualTo "") then {
|
||||
private _priceValue = _catalogEntry getOrDefault ["priceValue", 0];
|
||||
_total = _total + _priceValue;
|
||||
_resolvedUnits pushBack (createHashMapFromArray [
|
||||
|
||||
@ -530,7 +530,7 @@ GVAR(StorefrontBaseStore) = compileFinal createHashMapFromArray [
|
||||
_result
|
||||
};
|
||||
|
||||
private _checkoutRequest = GVAR(StoreCatalogService) call ["buildCheckoutRequest", [_items, _vehicles, _units]];
|
||||
private _checkoutRequest = GVAR(StoreCatalogService) call ["buildCheckoutRequest", [_items, _vehicles, _units, _player]];
|
||||
private _totalPrice = _checkoutRequest getOrDefault ["total", 0];
|
||||
|
||||
_result set ["paymentMethod", _paymentMethod];
|
||||
|
||||
@ -71,10 +71,17 @@ class CfgStore {
|
||||
};
|
||||
```
|
||||
|
||||
`dynamic` keeps the full generated catalog. `allowlist` only shows classnames
|
||||
listed for each category. `denylist` removes listed classnames. Overrides are
|
||||
server-side and are used by both the UI payload and checkout validation.
|
||||
`units[]` uses the same filter behavior as every other category.
|
||||
`modMode` is applied first. After that, any non-empty
|
||||
`Categories.<category>[]` array acts as an explicit allowlist for only that
|
||||
category, regardless of `mode`. Categories with empty arrays follow `mode`:
|
||||
`dynamic` keeps the generated entries and `allowlist` hides the category.
|
||||
Overrides are server-side and are used by both the UI payload and checkout
|
||||
validation.
|
||||
|
||||
Unit catalog responses are additionally filtered to the requesting player's
|
||||
side. Unit entries include side and faction metadata so the UI can show the
|
||||
available faction before purchase, and checkout validation rejects unit
|
||||
classnames from another side.
|
||||
|
||||
`modMode` applies before category filtering. `dynamic` means no mod-source
|
||||
filtering. `allowlist` only keeps generated entries that match one of the
|
||||
@ -93,9 +100,9 @@ modMode = "allowlist";
|
||||
mods[] = {"rhs"};
|
||||
```
|
||||
|
||||
The matching `class rhs` must exist under `ModSources`. Category `mode` is still
|
||||
applied afterward, so leave `mode = "dynamic"` if the mod filter should be the
|
||||
only inventory filter.
|
||||
The matching `class rhs` must exist under `ModSources`. Category arrays are
|
||||
still applied afterward, so leave arrays empty and `mode = "dynamic"` if the mod
|
||||
filter should be the only inventory filter.
|
||||
|
||||
For Creator DLCs such as RF or WS, prefer both prefixes and DLC labels:
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user