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/ui/map-viewer/
|
||||||
arma/mod/.hemttout/
|
arma/mod/.hemttout/
|
||||||
arma/server/surrealdb/forge.db/
|
arma/server/surrealdb/forge.db/
|
||||||
|
arma/temp/
|
||||||
promo/
|
promo/
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -94,6 +94,7 @@ ${scopeSelector} .product-card:hover {
|
|||||||
|
|
||||||
${scopeSelector} .card-kicker,
|
${scopeSelector} .card-kicker,
|
||||||
${scopeSelector} .product-code,
|
${scopeSelector} .product-code,
|
||||||
|
${scopeSelector} .product-tag,
|
||||||
${scopeSelector} .empty-state-kicker {
|
${scopeSelector} .empty-state-kicker {
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
letter-spacing: 0.14em;
|
letter-spacing: 0.14em;
|
||||||
@ -156,6 +157,21 @@ ${scopeSelector} .product-meta {
|
|||||||
gap: 0.2rem;
|
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 {
|
${scopeSelector} .product-name {
|
||||||
font-size: 0.96rem;
|
font-size: 0.96rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -370,6 +386,10 @@ ${scopeSelector} .product-copy {
|
|||||||
item.description,
|
item.description,
|
||||||
item.className || item.code,
|
item.className || item.code,
|
||||||
);
|
);
|
||||||
|
const tags = [
|
||||||
|
item.sideLabel ? item.sideLabel : "",
|
||||||
|
item.factionName ? item.factionName : "",
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
return h(
|
return h(
|
||||||
"article",
|
"article",
|
||||||
@ -399,6 +419,15 @@ ${scopeSelector} .product-copy {
|
|||||||
{ className: "product-code" },
|
{ className: "product-code" },
|
||||||
item.type || item.code || item.className,
|
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("strong", { className: "product-name" }, item.name),
|
||||||
),
|
),
|
||||||
h("p", { className: "product-copy" }, description),
|
h("p", { className: "product-copy" }, description),
|
||||||
|
|||||||
@ -195,6 +195,8 @@
|
|||||||
item.description,
|
item.description,
|
||||||
item.price,
|
item.price,
|
||||||
item.type,
|
item.type,
|
||||||
|
item.sideLabel,
|
||||||
|
item.factionName,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,10 @@
|
|||||||
type: String(item?.type || ""),
|
type: String(item?.type || ""),
|
||||||
category: String(item?.category || ""),
|
category: String(item?.category || ""),
|
||||||
entryKind: String(item?.entryKind || "item"),
|
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)),
|
quantity: Math.max(0, Number(item?.quantity || 0)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,10 +71,12 @@ class CfgStore {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
`dynamic` keeps the full generated catalog. `allowlist` only shows classnames
|
`modMode` is applied first. After that, any non-empty
|
||||||
listed for the requested category. `denylist` removes listed classnames from the
|
`Categories.<category>[]` array acts as an explicit allowlist for only that
|
||||||
generated category. Overrides are applied server-side, so checkout validation
|
category, regardless of `mode`. Categories with empty arrays follow `mode`:
|
||||||
uses the same prices and descriptions the UI displays.
|
`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
|
`modMode` applies before category filtering. `dynamic` means no mod-source
|
||||||
filtering. `allowlist` only keeps generated entries that match one of the
|
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
|
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.
|
treated as available and only the source/prefix/contains/DLC checks are used.
|
||||||
|
|
||||||
`units[]` follows the same `dynamic`, `allowlist`, and `denylist` behavior as
|
Unit catalog responses are additionally filtered to the requesting player's
|
||||||
item and vehicle categories. Unit purchases are immediate spawn grants, not
|
side. Unit entries include side and faction metadata for UI display, and
|
||||||
durable virtual garage unlocks.
|
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
|
The filter is currently global for the mission. Revisit per-store profile
|
||||||
support if individual vendors need different inventories.
|
support if individual vendors need different inventories.
|
||||||
|
|||||||
@ -20,7 +20,7 @@ PREP_RECOMPILE_END;
|
|||||||
diag_log "[FORGE:Server:Store] Store catalog service is unavailable."
|
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);
|
[CRPC(store,responseCategory), [_result], _player] call CFUNC(targetEvent);
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
|
|
||||||
|
|||||||
@ -294,21 +294,74 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
private _classNames = _self call ["getMissionStoreCategoryList", [_category]];
|
private _classNames = _self call ["getMissionStoreCategoryList", [_category]];
|
||||||
private _filteredItems = _self call ["applyMissionStoreModFilter", [_items]];
|
private _filteredItems = _self call ["applyMissionStoreModFilter", [_items]];
|
||||||
|
|
||||||
switch (_mode) do {
|
if (_classNames isNotEqualTo []) then {
|
||||||
case "allowlist": {
|
_filteredItems = _filteredItems select {
|
||||||
_filteredItems = _filteredItems select {
|
(toLowerANSI (_x getOrDefault ["className", ""])) in _classNames
|
||||||
(toLowerANSI (_x getOrDefault ["className", ""])) in _classNames
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
case "denylist": {
|
} else {
|
||||||
_filteredItems = _filteredItems select {
|
switch (_mode) do {
|
||||||
!((toLowerANSI (_x getOrDefault ["className", ""])) in _classNames)
|
case "allowlist": { _filteredItems = []; };
|
||||||
|
case "denylist": {
|
||||||
|
_filteredItems = _filteredItems select {
|
||||||
|
!((toLowerANSI (_x getOrDefault ["className", ""])) in _classNames)
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
_filteredItems apply { _self call ["applyMissionStoreOverrides", [_x]] }
|
_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 {
|
["formatCurrency", compileFinal {
|
||||||
params [["_amount", 0, [0]]];
|
params [["_amount", 0, [0]]];
|
||||||
|
|
||||||
@ -384,8 +437,7 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
};
|
};
|
||||||
|
|
||||||
private _priceValue = _self call ["calculateCatalogPriceValue", [_cfg, _isVehicle]];
|
private _priceValue = _self call ["calculateCatalogPriceValue", [_cfg, _isVehicle]];
|
||||||
|
private _item = createHashMapFromArray [
|
||||||
createHashMapFromArray [
|
|
||||||
["className", _className],
|
["className", _className],
|
||||||
["code", _className],
|
["code", _className],
|
||||||
["name", _displayName],
|
["name", _displayName],
|
||||||
@ -398,7 +450,26 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
["sourceMod", _sourceMod],
|
["sourceMod", _sourceMod],
|
||||||
["sourceDLC", _sourceDLC],
|
["sourceDLC", _sourceDLC],
|
||||||
["sourceAuthor", _sourceAuthor]
|
["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 {
|
["appendCfgWeaponsByItemInfoType", compileFinal {
|
||||||
params [["_items", [], [[]]], ["_itemInfoType", -1, [0]], ["_itemKind", "", [""]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]];
|
params [["_items", [], [[]]], ["_itemInfoType", -1, [0]], ["_itemKind", "", [""]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]];
|
||||||
@ -686,7 +757,7 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
_items
|
_items
|
||||||
}],
|
}],
|
||||||
["buildCategoryResponse", compileFinal {
|
["buildCategoryResponse", compileFinal {
|
||||||
params [["_category", "", [""]]];
|
params [["_category", "", [""]], ["_player", objNull, [objNull]]];
|
||||||
|
|
||||||
private _categoryKey = _self call ["normalizeCategoryKey", [_category]];
|
private _categoryKey = _self call ["normalizeCategoryKey", [_category]];
|
||||||
private _response = createHashMapFromArray [["success", false], ["category", _categoryKey], ["items", []], ["message", "No store category was provided."]];
|
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 ["success", true];
|
||||||
_response set ["message", ""];
|
_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
|
_response
|
||||||
}],
|
}],
|
||||||
["resolveCheckoutCategories", compileFinal {
|
["resolveCheckoutCategories", compileFinal {
|
||||||
@ -734,7 +808,7 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
_resolved
|
_resolved
|
||||||
}],
|
}],
|
||||||
["buildCheckoutRequest", compileFinal {
|
["buildCheckoutRequest", compileFinal {
|
||||||
params [["_items", [], [[]]], ["_vehicles", [], [[]]], ["_units", [], [[]]]];
|
params [["_items", [], [[]]], ["_vehicles", [], [[]]], ["_units", [], [[]]], ["_player", objNull, [objNull]]];
|
||||||
|
|
||||||
private _result = createHashMapFromArray [
|
private _result = createHashMapFromArray [
|
||||||
["success", false],
|
["success", false],
|
||||||
@ -810,6 +884,12 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
if (_catalogEntry isEqualTo createHashMap) then {
|
if (_catalogEntry isEqualTo createHashMap) then {
|
||||||
_message = format ["Unsupported store unit: %1", _className];
|
_message = format ["Unsupported store unit: %1", _className];
|
||||||
} else {
|
} 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];
|
private _priceValue = _catalogEntry getOrDefault ["priceValue", 0];
|
||||||
_total = _total + _priceValue;
|
_total = _total + _priceValue;
|
||||||
_resolvedUnits pushBack (createHashMapFromArray [
|
_resolvedUnits pushBack (createHashMapFromArray [
|
||||||
|
|||||||
@ -530,7 +530,7 @@ GVAR(StorefrontBaseStore) = compileFinal createHashMapFromArray [
|
|||||||
_result
|
_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];
|
private _totalPrice = _checkoutRequest getOrDefault ["total", 0];
|
||||||
|
|
||||||
_result set ["paymentMethod", _paymentMethod];
|
_result set ["paymentMethod", _paymentMethod];
|
||||||
|
|||||||
@ -71,10 +71,17 @@ class CfgStore {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
`dynamic` keeps the full generated catalog. `allowlist` only shows classnames
|
`modMode` is applied first. After that, any non-empty
|
||||||
listed for each category. `denylist` removes listed classnames. Overrides are
|
`Categories.<category>[]` array acts as an explicit allowlist for only that
|
||||||
server-side and are used by both the UI payload and checkout validation.
|
category, regardless of `mode`. Categories with empty arrays follow `mode`:
|
||||||
`units[]` uses the same filter behavior as every other category.
|
`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
|
`modMode` applies before category filtering. `dynamic` means no mod-source
|
||||||
filtering. `allowlist` only keeps generated entries that match one of the
|
filtering. `allowlist` only keeps generated entries that match one of the
|
||||||
@ -93,9 +100,9 @@ modMode = "allowlist";
|
|||||||
mods[] = {"rhs"};
|
mods[] = {"rhs"};
|
||||||
```
|
```
|
||||||
|
|
||||||
The matching `class rhs` must exist under `ModSources`. Category `mode` is still
|
The matching `class rhs` must exist under `ModSources`. Category arrays are
|
||||||
applied afterward, so leave `mode = "dynamic"` if the mod filter should be the
|
still applied afterward, so leave arrays empty and `mode = "dynamic"` if the mod
|
||||||
only inventory filter.
|
filter should be the only inventory filter.
|
||||||
|
|
||||||
For Creator DLCs such as RF or WS, prefer both prefixes and DLC labels:
|
For Creator DLCs such as RF or WS, prefer both prefixes and DLC labels:
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user