@pixels-online/pixels-client-js-sdk 1.15.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/OfferwallClient.d.ts +28 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +313 -59
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +314 -58
- package/dist/index.js.map +1 -1
- package/dist/offerwall-sdk.umd.js +314 -58
- package/dist/offerwall-sdk.umd.js.map +1 -1
- package/dist/types/blockchain/user_wallet.d.ts +19 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/linking.d.ts +17 -0
- package/dist/types/offer.d.ts +65 -17
- package/dist/types/player.d.ts +25 -11
- package/dist/utils/conditions.d.ts +50 -9
- package/dist/utils/template.d.ts +18 -1
- package/package.json +2 -1
|
@@ -896,6 +896,7 @@
|
|
|
896
896
|
class OfferwallClient {
|
|
897
897
|
constructor(config) {
|
|
898
898
|
this.isInitializing = false;
|
|
899
|
+
this.pendingStackedToken = null;
|
|
899
900
|
this.config = {
|
|
900
901
|
autoConnect: config.autoConnect ?? false,
|
|
901
902
|
reconnect: config.reconnect ?? true,
|
|
@@ -913,12 +914,24 @@
|
|
|
913
914
|
this.assetHelper = new AssetHelper(this.config);
|
|
914
915
|
this.sseConnection = new SSEConnection(this.config, this.eventEmitter, this.tokenManager);
|
|
915
916
|
this.setupInternalListeners();
|
|
917
|
+
this.setupStackedLinkDetection();
|
|
916
918
|
if (this.config.autoConnect) {
|
|
917
919
|
this.initialize().catch((err) => {
|
|
918
920
|
this.logger.error('Auto-initialization failed:', err);
|
|
919
921
|
});
|
|
920
922
|
}
|
|
921
923
|
}
|
|
924
|
+
setupStackedLinkDetection() {
|
|
925
|
+
if (!this.config.stackedLink?.autoConsume) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const token = this.detectStackedLinkToken();
|
|
929
|
+
if (token) {
|
|
930
|
+
this.logger.log('Detected stackedToken in URL');
|
|
931
|
+
this.pendingStackedToken = token;
|
|
932
|
+
this.clearStackedTokenFromUrl();
|
|
933
|
+
}
|
|
934
|
+
}
|
|
922
935
|
/**
|
|
923
936
|
* Get the offer store instance
|
|
924
937
|
*/
|
|
@@ -949,9 +962,19 @@
|
|
|
949
962
|
this.isInitializing = true;
|
|
950
963
|
await this.refreshOffersAndPlayer();
|
|
951
964
|
await this.connect();
|
|
965
|
+
// Process pending Stacked link token if exists and autoConsume is enabled
|
|
966
|
+
if (this.pendingStackedToken && this.config.stackedLink?.autoConsume) {
|
|
967
|
+
this.logger.log('Processing pending Stacked link token');
|
|
968
|
+
const result = await this.consumeStackedLinkToken(this.pendingStackedToken);
|
|
969
|
+
this.pendingStackedToken = null;
|
|
970
|
+
if (this.config.stackedLink?.onComplete) {
|
|
971
|
+
this.config.stackedLink.onComplete(result);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
952
974
|
this.isInitializing = false;
|
|
953
975
|
}
|
|
954
976
|
catch (error) {
|
|
977
|
+
this.isInitializing = false;
|
|
955
978
|
this.handleError(error, 'initialize');
|
|
956
979
|
throw error;
|
|
957
980
|
}
|
|
@@ -1167,6 +1190,91 @@
|
|
|
1167
1190
|
return null;
|
|
1168
1191
|
return `${dashboardBaseUrl}/auth/enter?token=${token}&gameId=${gameId}`;
|
|
1169
1192
|
}
|
|
1193
|
+
// ==================== Stacked Link Methods ====================
|
|
1194
|
+
/**
|
|
1195
|
+
* Detect if there's a Stacked link token in the current URL
|
|
1196
|
+
* @returns The token string if found, null otherwise
|
|
1197
|
+
*/
|
|
1198
|
+
detectStackedLinkToken() {
|
|
1199
|
+
if (typeof window === 'undefined')
|
|
1200
|
+
return null;
|
|
1201
|
+
try {
|
|
1202
|
+
const params = new URLSearchParams(window.location.search);
|
|
1203
|
+
const token = params.get('stackedToken');
|
|
1204
|
+
if (token && token.length > 0) {
|
|
1205
|
+
return token;
|
|
1206
|
+
}
|
|
1207
|
+
// for SPA routers that use hash routing
|
|
1208
|
+
if (window.location.hash) {
|
|
1209
|
+
const hashQuery = window.location.hash.split('?')[1];
|
|
1210
|
+
if (hashQuery) {
|
|
1211
|
+
const hashParams = new URLSearchParams(hashQuery);
|
|
1212
|
+
const hashToken = hashParams.get('stackedToken');
|
|
1213
|
+
if (hashToken && hashToken.length > 0) {
|
|
1214
|
+
return hashToken;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
catch (error) {
|
|
1221
|
+
this.logger.error('Error detecting stacked token:', error);
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Clear the stackedToken from the URL without page reload
|
|
1227
|
+
*/
|
|
1228
|
+
clearStackedTokenFromUrl() {
|
|
1229
|
+
if (typeof window === 'undefined')
|
|
1230
|
+
return;
|
|
1231
|
+
try {
|
|
1232
|
+
const url = new URL(window.location.href);
|
|
1233
|
+
url.searchParams.delete('stackedToken');
|
|
1234
|
+
window.history.replaceState({}, '', url.toString());
|
|
1235
|
+
this.logger.log('Cleared stackedToken from URL');
|
|
1236
|
+
}
|
|
1237
|
+
catch (error) {
|
|
1238
|
+
this.logger.error('Error clearing stacked token from URL:', error);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Consume a Stacked link token to link the current game player
|
|
1243
|
+
* to a Stacked unified user account.
|
|
1244
|
+
*
|
|
1245
|
+
* IMPORTANT: The player must be authenticated (have a valid JWT from tokenProvider)
|
|
1246
|
+
* before calling this method.
|
|
1247
|
+
*
|
|
1248
|
+
* @param token The Stacked link token from the URL
|
|
1249
|
+
* @returns Promise resolving to the link result
|
|
1250
|
+
*/
|
|
1251
|
+
async consumeStackedLinkToken(token) {
|
|
1252
|
+
try {
|
|
1253
|
+
const data = await this.postWithAuth('/v1/auth/stacked_link/exchange', { stackedToken: token });
|
|
1254
|
+
return {
|
|
1255
|
+
linked: data.linked,
|
|
1256
|
+
alreadyLinked: data.alreadyLinked,
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
catch (error) {
|
|
1260
|
+
this.logger.error('Error consuming stacked link token:', error);
|
|
1261
|
+
return {
|
|
1262
|
+
linked: false,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Detects the token from URL, consumes it if player is authenticated.
|
|
1268
|
+
* @returns Promise resolving to the link result, or null if no token found
|
|
1269
|
+
*/
|
|
1270
|
+
async processStackedLinkToken() {
|
|
1271
|
+
const token = this.detectStackedLinkToken();
|
|
1272
|
+
if (!token) {
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
this.clearStackedTokenFromUrl();
|
|
1276
|
+
return this.consumeStackedLinkToken(token);
|
|
1277
|
+
}
|
|
1170
1278
|
handleError(error, context) {
|
|
1171
1279
|
this.logger.error(`Error in ${context}:`, error);
|
|
1172
1280
|
if (this.hooks.onError) {
|
|
@@ -1176,19 +1284,47 @@
|
|
|
1176
1284
|
}
|
|
1177
1285
|
|
|
1178
1286
|
const keyPattern = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
1179
|
-
|
|
1287
|
+
/**
|
|
1288
|
+
* This replaces {keyName} keys from the template with corresponding values from the dynamic object.
|
|
1289
|
+
*/
|
|
1180
1290
|
function renderTemplate(template, dynamic) {
|
|
1181
1291
|
if (!template)
|
|
1182
1292
|
return '';
|
|
1183
|
-
return template.replace(keyPattern, (
|
|
1293
|
+
return template.replace(keyPattern, (_match, key) => {
|
|
1294
|
+
if (dynamic && typeof dynamic[key] === 'boolean') {
|
|
1295
|
+
return dynamic[key] ? '✓' : '✗';
|
|
1296
|
+
}
|
|
1184
1297
|
if (dynamic && dynamic[key] !== undefined) {
|
|
1185
1298
|
return String(dynamic[key]);
|
|
1186
1299
|
}
|
|
1187
1300
|
return '{?}'; // indicate missing key
|
|
1188
1301
|
});
|
|
1189
1302
|
}
|
|
1303
|
+
/**
|
|
1304
|
+
* This replaces {{keyName}} in dynamic condition keys with corresponding values from
|
|
1305
|
+
* the PlayerOffer.trackers
|
|
1306
|
+
*
|
|
1307
|
+
* eg. a condition high_score_pet-{{surfacerPlayerId}} with high_score_pet-12345
|
|
1308
|
+
*/
|
|
1309
|
+
function replaceDynamicConditionKey(key, trackers) {
|
|
1310
|
+
return key?.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (match, p1) => {
|
|
1311
|
+
const value = trackers[p1];
|
|
1312
|
+
return value !== undefined ? String(value) : match;
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
/** this replaces all of the dynamic conditions.keys by calling replaceDynamicConditionKey */
|
|
1316
|
+
function replaceDynamicConditionKeys(conditions, trackers) {
|
|
1317
|
+
return conditions.map((condition) => ({
|
|
1318
|
+
...condition,
|
|
1319
|
+
key: replaceDynamicConditionKey(condition.key, trackers),
|
|
1320
|
+
}));
|
|
1321
|
+
}
|
|
1190
1322
|
|
|
1191
|
-
const meetsBaseConditions = ({ conditions, playerSnap, addDetails,
|
|
1323
|
+
const meetsBaseConditions = ({ conditions, playerSnap, addDetails,
|
|
1324
|
+
/** this exists if calling meetsBaseConditions from meetsCompletionConditions. but surfacing
|
|
1325
|
+
* check doesn't use this since we don't have a playerOffer at surfacing time
|
|
1326
|
+
*/
|
|
1327
|
+
playerOffer, }) => {
|
|
1192
1328
|
const conditionData = [];
|
|
1193
1329
|
let isValid = true;
|
|
1194
1330
|
if (conditions?.minDaysInGame) {
|
|
@@ -1514,7 +1650,7 @@
|
|
|
1514
1650
|
}
|
|
1515
1651
|
}
|
|
1516
1652
|
// Validate link count conditions
|
|
1517
|
-
if (conditions?.links) {
|
|
1653
|
+
if (conditions?.links && 'entityLinks' in playerSnap) {
|
|
1518
1654
|
for (const [linkType, constraint] of Object.entries(conditions.links)) {
|
|
1519
1655
|
const linkCount = playerSnap.entityLinks?.filter((link) => link.kind === linkType).length || 0;
|
|
1520
1656
|
if (constraint.min !== undefined) {
|
|
@@ -1555,7 +1691,11 @@
|
|
|
1555
1691
|
}
|
|
1556
1692
|
// Evaluate dynamic conditions
|
|
1557
1693
|
if (conditions?.dynamic?.conditions?.length) {
|
|
1558
|
-
const
|
|
1694
|
+
const resolvedConditions = replaceDynamicConditionKeys(conditions.dynamic.conditions, playerOffer?.trackers || {});
|
|
1695
|
+
const dynamicResult = meetsDynamicConditions(playerSnap.dynamic, {
|
|
1696
|
+
...conditions.dynamic,
|
|
1697
|
+
conditions: resolvedConditions,
|
|
1698
|
+
});
|
|
1559
1699
|
if (addDetails) {
|
|
1560
1700
|
conditionData.push({
|
|
1561
1701
|
isMet: dynamicResult,
|
|
@@ -1571,7 +1711,7 @@
|
|
|
1571
1711
|
return { isValid: false };
|
|
1572
1712
|
}
|
|
1573
1713
|
}
|
|
1574
|
-
if (conditions?.identifiers?.platforms?.length) {
|
|
1714
|
+
if (conditions?.identifiers?.platforms?.length && 'identifiers' in playerSnap) {
|
|
1575
1715
|
const playerPlatforms = new Set(playerSnap.identifiers?.map((i) => i.platform.toLowerCase()) || []);
|
|
1576
1716
|
const isAndBehaviour = conditions.identifiers.behaviour === 'AND';
|
|
1577
1717
|
const platformsToCheck = conditions.identifiers.platforms;
|
|
@@ -1608,7 +1748,7 @@
|
|
|
1608
1748
|
return { isValid: false };
|
|
1609
1749
|
}
|
|
1610
1750
|
if (surfacingConditions?.targetEntityTypes?.length) {
|
|
1611
|
-
const playerTarget = playerSnap.
|
|
1751
|
+
const playerTarget = playerSnap.entityKind || 'default';
|
|
1612
1752
|
// check if entity type is allowed
|
|
1613
1753
|
if (!surfacingConditions.targetEntityTypes.includes(playerTarget)) {
|
|
1614
1754
|
return { isValid: false };
|
|
@@ -1664,6 +1804,31 @@
|
|
|
1664
1804
|
return { isValid: false };
|
|
1665
1805
|
}
|
|
1666
1806
|
}
|
|
1807
|
+
if (conditions.allowedCountries?.length) {
|
|
1808
|
+
const playerCountry = playerSnap.ip?.countryCode;
|
|
1809
|
+
if (!playerCountry || !conditions.allowedCountries.includes(playerCountry)) {
|
|
1810
|
+
return { isValid: false };
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
if (conditions.restrictedCountries?.length) {
|
|
1814
|
+
const playerCountry = playerSnap.ip?.countryCode;
|
|
1815
|
+
if (!playerCountry) {
|
|
1816
|
+
return { isValid: false };
|
|
1817
|
+
}
|
|
1818
|
+
if (conditions.restrictedCountries.includes(playerCountry)) {
|
|
1819
|
+
return { isValid: false };
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
if (conditions.networkRestrictions?.length) {
|
|
1823
|
+
if (!playerSnap.ip) {
|
|
1824
|
+
return { isValid: false };
|
|
1825
|
+
}
|
|
1826
|
+
for (const restriction of conditions.networkRestrictions) {
|
|
1827
|
+
if (playerSnap.ip[restriction]) {
|
|
1828
|
+
return { isValid: false };
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1667
1832
|
return meetsBaseConditions({ conditions, playerSnap });
|
|
1668
1833
|
};
|
|
1669
1834
|
const hasConditions = (conditions) => {
|
|
@@ -1716,6 +1881,12 @@
|
|
|
1716
1881
|
return true;
|
|
1717
1882
|
if (surCond.links && Object.keys(surCond.links).length > 0)
|
|
1718
1883
|
return true;
|
|
1884
|
+
if (surCond.allowedCountries?.length)
|
|
1885
|
+
return true;
|
|
1886
|
+
if (surCond.restrictedCountries?.length)
|
|
1887
|
+
return true;
|
|
1888
|
+
if (surCond.networkRestrictions?.length)
|
|
1889
|
+
return true;
|
|
1719
1890
|
const compCond = conditions;
|
|
1720
1891
|
if (compCond.context)
|
|
1721
1892
|
return true;
|
|
@@ -1733,11 +1904,25 @@
|
|
|
1733
1904
|
return true;
|
|
1734
1905
|
if (compCond.linkedCompletions)
|
|
1735
1906
|
return true;
|
|
1907
|
+
if (compCond.dynamicTracker?.conditions?.length)
|
|
1908
|
+
return true;
|
|
1736
1909
|
return false;
|
|
1737
1910
|
};
|
|
1738
|
-
const
|
|
1911
|
+
const offerMeetsCompletionConditions = (offer, snapshot) => {
|
|
1912
|
+
return meetsCompletionConditions({
|
|
1913
|
+
completionConditions: offer.completionConditions || {},
|
|
1914
|
+
completionTrackers: offer.completionTrackers,
|
|
1915
|
+
playerSnap: snapshot,
|
|
1916
|
+
playerOffer: offer,
|
|
1917
|
+
addDetails: true,
|
|
1918
|
+
});
|
|
1919
|
+
};
|
|
1920
|
+
const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, }) => {
|
|
1739
1921
|
if (completionConditions) {
|
|
1740
1922
|
const conditions = completionConditions;
|
|
1923
|
+
// For multi-claim offers, scale cumulative requirements by (claimedCount + 1)
|
|
1924
|
+
const shouldScale = maxClaimCount === -1 || (maxClaimCount && maxClaimCount > 1);
|
|
1925
|
+
const claimMultiplier = shouldScale ? (playerOffer.claimedCount || 0) + 1 : 1;
|
|
1741
1926
|
const conditionData = [];
|
|
1742
1927
|
let isValid = true;
|
|
1743
1928
|
if (completionConditions?.context?.id) {
|
|
@@ -1760,13 +1945,14 @@
|
|
|
1760
1945
|
}
|
|
1761
1946
|
}
|
|
1762
1947
|
if (conditions?.buyItem) {
|
|
1763
|
-
const
|
|
1948
|
+
const scaledAmount = (conditions.buyItem.amount || 1) * claimMultiplier;
|
|
1949
|
+
const isDisqualify = (completionTrackers?.buyItem || 0) < scaledAmount;
|
|
1764
1950
|
if (addDetails) {
|
|
1765
1951
|
conditionData.push({
|
|
1766
1952
|
isMet: !isDisqualify,
|
|
1767
1953
|
kind: 'buyItem',
|
|
1768
1954
|
trackerAmount: completionTrackers?.buyItem || 0,
|
|
1769
|
-
text: `Buy ${
|
|
1955
|
+
text: `Buy ${scaledAmount} ${conditions.buyItem.name}`,
|
|
1770
1956
|
});
|
|
1771
1957
|
if (isDisqualify)
|
|
1772
1958
|
isValid = false;
|
|
@@ -1777,13 +1963,14 @@
|
|
|
1777
1963
|
}
|
|
1778
1964
|
}
|
|
1779
1965
|
if (conditions?.spendCurrency) {
|
|
1780
|
-
const
|
|
1966
|
+
const scaledAmount = (conditions.spendCurrency.amount || 1) * claimMultiplier;
|
|
1967
|
+
const isDisqualify = (completionTrackers?.spendCurrency || 0) < scaledAmount;
|
|
1781
1968
|
if (addDetails) {
|
|
1782
1969
|
conditionData.push({
|
|
1783
1970
|
isMet: !isDisqualify,
|
|
1784
1971
|
kind: 'spendCurrency',
|
|
1785
1972
|
trackerAmount: completionTrackers?.spendCurrency || 0,
|
|
1786
|
-
text: `Spend ${
|
|
1973
|
+
text: `Spend ${scaledAmount} ${conditions.spendCurrency.name}`,
|
|
1787
1974
|
});
|
|
1788
1975
|
if (isDisqualify)
|
|
1789
1976
|
isValid = false;
|
|
@@ -1794,15 +1981,17 @@
|
|
|
1794
1981
|
}
|
|
1795
1982
|
}
|
|
1796
1983
|
if (conditions?.depositCurrency) {
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1984
|
+
const scaledAmount = (conditions.depositCurrency.amount || 1) * claimMultiplier;
|
|
1985
|
+
const isDisqualify = (completionTrackers?.depositCurrency || 0) < scaledAmount;
|
|
1799
1986
|
if (addDetails) {
|
|
1800
1987
|
conditionData.push({
|
|
1801
1988
|
isMet: !isDisqualify,
|
|
1802
1989
|
kind: 'depositCurrency',
|
|
1803
1990
|
trackerAmount: completionTrackers?.depositCurrency || 0,
|
|
1804
|
-
text: `Deposit ${
|
|
1991
|
+
text: `Deposit ${scaledAmount} ${conditions.depositCurrency.name}`,
|
|
1805
1992
|
});
|
|
1993
|
+
if (isDisqualify)
|
|
1994
|
+
isValid = false;
|
|
1806
1995
|
}
|
|
1807
1996
|
else {
|
|
1808
1997
|
if (isDisqualify)
|
|
@@ -1810,12 +1999,13 @@
|
|
|
1810
1999
|
}
|
|
1811
2000
|
}
|
|
1812
2001
|
if (conditions?.login) {
|
|
1813
|
-
const isMet =
|
|
2002
|
+
const isMet = new Date(playerSnap.snapshotLastUpdated || 0).getTime() >
|
|
2003
|
+
new Date(playerOffer.createdAt || 0).getTime();
|
|
1814
2004
|
if (addDetails) {
|
|
1815
2005
|
conditionData.push({
|
|
1816
2006
|
isMet,
|
|
1817
2007
|
kind: 'login',
|
|
1818
|
-
trackerAmount:
|
|
2008
|
+
trackerAmount: isMet ? 1 : 0,
|
|
1819
2009
|
text: `Login to the game`,
|
|
1820
2010
|
});
|
|
1821
2011
|
isValid = isMet;
|
|
@@ -1852,9 +2042,11 @@
|
|
|
1852
2042
|
const hasContent = Boolean(mode === 'accumulate'
|
|
1853
2043
|
? tSocial?.mode === 'accumulate'
|
|
1854
2044
|
: tSocial && tSocial.mode !== 'accumulate' && !!tSocial.videoId);
|
|
1855
|
-
|
|
1856
|
-
const
|
|
1857
|
-
const
|
|
2045
|
+
// Only scale social metrics in accumulate mode (attach mode is single content)
|
|
2046
|
+
const socialMultiplier = mode === 'accumulate' ? claimMultiplier : 1;
|
|
2047
|
+
const minLikes = (cSocial?.minLikes || 0) * socialMultiplier;
|
|
2048
|
+
const minViews = (cSocial?.minViews || 0) * socialMultiplier;
|
|
2049
|
+
const minComments = (cSocial?.minComments || 0) * socialMultiplier;
|
|
1858
2050
|
const likes = tSocial?.likes || 0;
|
|
1859
2051
|
const views = tSocial?.views || 0;
|
|
1860
2052
|
const comments = tSocial?.comments || 0;
|
|
@@ -1939,14 +2131,14 @@
|
|
|
1939
2131
|
// Linked completions - wait for N linked entities to complete
|
|
1940
2132
|
if (conditions?.linkedCompletions?.min) {
|
|
1941
2133
|
const currentCount = completionTrackers?.linkedCompletions || 0;
|
|
1942
|
-
const
|
|
1943
|
-
const isDisqualify = currentCount <
|
|
2134
|
+
const scaledMin = conditions.linkedCompletions.min * claimMultiplier;
|
|
2135
|
+
const isDisqualify = currentCount < scaledMin;
|
|
1944
2136
|
if (addDetails) {
|
|
1945
2137
|
conditionData.push({
|
|
1946
2138
|
isMet: !isDisqualify,
|
|
1947
2139
|
kind: 'linkedCompletions',
|
|
1948
2140
|
trackerAmount: currentCount,
|
|
1949
|
-
text: `Wait for ${
|
|
2141
|
+
text: `Wait for ${scaledMin} linked ${scaledMin === 1 ? 'entity' : 'entities'} to complete`,
|
|
1950
2142
|
});
|
|
1951
2143
|
if (isDisqualify)
|
|
1952
2144
|
isValid = false;
|
|
@@ -1956,10 +2148,32 @@
|
|
|
1956
2148
|
return { isValid: false };
|
|
1957
2149
|
}
|
|
1958
2150
|
}
|
|
2151
|
+
if (conditions?.dynamicTracker?.conditions?.length) {
|
|
2152
|
+
const resolvedConditions = replaceDynamicConditionKeys(conditions.dynamicTracker.conditions, playerOffer?.trackers || {});
|
|
2153
|
+
// now we have the game-defined conditions with {{}} keys populated. feed these conditions into evaluator
|
|
2154
|
+
const dynamicResult = meetsDynamicConditions(completionTrackers?.dynamicTracker, {
|
|
2155
|
+
...conditions.dynamicTracker,
|
|
2156
|
+
conditions: resolvedConditions,
|
|
2157
|
+
}, claimMultiplier);
|
|
2158
|
+
if (addDetails) {
|
|
2159
|
+
conditionData.push({
|
|
2160
|
+
isMet: dynamicResult,
|
|
2161
|
+
kind: 'dynamic',
|
|
2162
|
+
text: renderTemplate(conditions.dynamicTracker.template, completionTrackers?.dynamicTracker) || 'Dynamic conditions',
|
|
2163
|
+
});
|
|
2164
|
+
if (!dynamicResult)
|
|
2165
|
+
isValid = false;
|
|
2166
|
+
}
|
|
2167
|
+
else {
|
|
2168
|
+
if (!dynamicResult)
|
|
2169
|
+
return { isValid: false };
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
1959
2172
|
const r = meetsBaseConditions({
|
|
1960
2173
|
conditions,
|
|
1961
2174
|
playerSnap,
|
|
1962
2175
|
addDetails: true,
|
|
2176
|
+
playerOffer,
|
|
1963
2177
|
});
|
|
1964
2178
|
isValid = isValid && r.isValid;
|
|
1965
2179
|
conditionData.push(...(r.conditionData || []));
|
|
@@ -1974,10 +2188,9 @@
|
|
|
1974
2188
|
* @param completionConditions - The completion conditions to check
|
|
1975
2189
|
* @param completionTrackers - The completion trackers (for buyItem, spendCurrency, etc.)
|
|
1976
2190
|
* @param playerSnap - The player snapshot with field timestamps
|
|
1977
|
-
* @param expiryTime - The expiry timestamp in milliseconds
|
|
1978
2191
|
* @returns true if all conditions were met before expiry, false otherwise
|
|
1979
2192
|
*/
|
|
1980
|
-
const meetsCompletionConditionsBeforeExpiry = ({ completionConditions, completionTrackers, playerSnap,
|
|
2193
|
+
const meetsCompletionConditionsBeforeExpiry = ({ completionConditions, completionTrackers, playerSnap, playerOffer, maxClaimCount, }) => {
|
|
1981
2194
|
if (!completionConditions)
|
|
1982
2195
|
return false;
|
|
1983
2196
|
// Check if there are actually any conditions to evaluate
|
|
@@ -1987,10 +2200,15 @@
|
|
|
1987
2200
|
const conditionsMet = meetsCompletionConditions({
|
|
1988
2201
|
completionConditions,
|
|
1989
2202
|
completionTrackers,
|
|
2203
|
+
playerOffer,
|
|
1990
2204
|
playerSnap,
|
|
2205
|
+
maxClaimCount,
|
|
1991
2206
|
});
|
|
1992
2207
|
if (!conditionsMet.isValid)
|
|
1993
2208
|
return false;
|
|
2209
|
+
if (!playerOffer.expiresAt)
|
|
2210
|
+
return true;
|
|
2211
|
+
const expiryTime = new Date(playerOffer.expiresAt).getTime();
|
|
1994
2212
|
const lastSnapshotUpdate = new Date(playerSnap.snapshotLastUpdated).getTime();
|
|
1995
2213
|
/**
|
|
1996
2214
|
* Checks if a field was updated after the expiry time.
|
|
@@ -2101,66 +2319,80 @@
|
|
|
2101
2319
|
* Checks if a dynamic object meets a set of dynamic field conditions.
|
|
2102
2320
|
* @param dynamicObj - The object with any key and string or number value.
|
|
2103
2321
|
* @param conditions - Array of conditions to check.
|
|
2322
|
+
* @param claimMultiplier - Multiplier to scale conditions (used for numeric comparisons).
|
|
2104
2323
|
* @returns true if all conditions are met, false otherwise.
|
|
2105
2324
|
*/
|
|
2106
2325
|
/**
|
|
2107
2326
|
* Evaluates a single dynamic condition against the dynamic object.
|
|
2108
2327
|
*/
|
|
2109
|
-
function evaluateDynamicCondition(dynamicObj, cond) {
|
|
2328
|
+
function evaluateDynamicCondition(dynamicObj, cond, claimMultiplier = 1) {
|
|
2329
|
+
if (!dynamicObj)
|
|
2330
|
+
return false;
|
|
2110
2331
|
const val = dynamicObj[cond.key];
|
|
2111
2332
|
if (val === undefined)
|
|
2112
2333
|
return false;
|
|
2334
|
+
const isNumber = typeof val === 'number';
|
|
2335
|
+
const isBoolean = typeof val === 'boolean';
|
|
2336
|
+
if (isBoolean) {
|
|
2337
|
+
switch (cond.operator) {
|
|
2338
|
+
case '==':
|
|
2339
|
+
return val === Boolean(cond.compareTo);
|
|
2340
|
+
case '!=':
|
|
2341
|
+
return val !== Boolean(cond.compareTo);
|
|
2342
|
+
default:
|
|
2343
|
+
return false;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
const compareTo = isNumber ? Number(cond.compareTo) : String(cond.compareTo);
|
|
2113
2347
|
switch (cond.operator) {
|
|
2114
2348
|
case '==':
|
|
2115
|
-
return val ===
|
|
2349
|
+
return val === compareTo;
|
|
2116
2350
|
case '!=':
|
|
2117
|
-
return val !==
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
case '<=':
|
|
2131
|
-
return (typeof val === 'number' &&
|
|
2132
|
-
typeof cond.compareTo === 'number' &&
|
|
2133
|
-
val <= cond.compareTo);
|
|
2134
|
-
case 'has':
|
|
2135
|
-
return (typeof val === 'string' &&
|
|
2136
|
-
typeof cond.compareTo === 'string' &&
|
|
2137
|
-
val.includes(cond.compareTo));
|
|
2138
|
-
case 'not_has':
|
|
2139
|
-
return (typeof val === 'string' &&
|
|
2140
|
-
typeof cond.compareTo === 'string' &&
|
|
2141
|
-
!val.includes(cond.compareTo));
|
|
2142
|
-
default:
|
|
2143
|
-
return false;
|
|
2351
|
+
return val !== compareTo;
|
|
2352
|
+
}
|
|
2353
|
+
if (isNumber && typeof compareTo === 'number') {
|
|
2354
|
+
switch (cond.operator) {
|
|
2355
|
+
case '>':
|
|
2356
|
+
return val > compareTo * claimMultiplier;
|
|
2357
|
+
case '>=':
|
|
2358
|
+
return val >= compareTo * claimMultiplier;
|
|
2359
|
+
case '<':
|
|
2360
|
+
return val < compareTo * claimMultiplier;
|
|
2361
|
+
case '<=':
|
|
2362
|
+
return val <= compareTo * claimMultiplier;
|
|
2363
|
+
}
|
|
2144
2364
|
}
|
|
2365
|
+
else if (!isNumber && typeof compareTo === 'string') {
|
|
2366
|
+
switch (cond.operator) {
|
|
2367
|
+
case 'has':
|
|
2368
|
+
return val.includes(compareTo);
|
|
2369
|
+
case 'not_has':
|
|
2370
|
+
return !val.includes(compareTo);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
return false;
|
|
2145
2374
|
}
|
|
2146
2375
|
/**
|
|
2147
2376
|
* Evaluates a group of dynamic conditions with logical links (AND, OR, AND NOT).
|
|
2148
2377
|
* @param dynamicObj - The player's dynamic object with any key and string or number value.
|
|
2149
2378
|
* @param dynamicGroup - The group of conditions and links to check.
|
|
2379
|
+
* @param claimMultiplier - Multiplier to scale conditions (used for numeric comparisons).
|
|
2150
2380
|
* @returns true if the group evaluates to true, false otherwise.
|
|
2151
2381
|
*/
|
|
2152
|
-
function meetsDynamicConditions(dynamicObj, dynamicGroup) {
|
|
2382
|
+
function meetsDynamicConditions(dynamicObj, dynamicGroup, claimMultiplier = 1) {
|
|
2153
2383
|
const { conditions, links } = dynamicGroup;
|
|
2154
2384
|
if (!conditions || conditions.length === 0)
|
|
2155
2385
|
return true;
|
|
2386
|
+
if (!dynamicObj)
|
|
2387
|
+
return false;
|
|
2156
2388
|
// If no links, treat as AND between all conditions
|
|
2157
2389
|
if (!links || links.length === 0) {
|
|
2158
|
-
return conditions.every((cond) => evaluateDynamicCondition(dynamicObj, cond));
|
|
2390
|
+
return conditions.every((cond) => evaluateDynamicCondition(dynamicObj, cond, claimMultiplier));
|
|
2159
2391
|
}
|
|
2160
2392
|
// Evaluate the first condition
|
|
2161
|
-
let result = evaluateDynamicCondition(dynamicObj, conditions[0]);
|
|
2393
|
+
let result = evaluateDynamicCondition(dynamicObj, conditions[0], claimMultiplier);
|
|
2162
2394
|
for (let i = 0; i < links.length; i++) {
|
|
2163
|
-
const nextCond = evaluateDynamicCondition(dynamicObj, conditions[i + 1]);
|
|
2395
|
+
const nextCond = evaluateDynamicCondition(dynamicObj, conditions[i + 1], claimMultiplier);
|
|
2164
2396
|
const link = links[i];
|
|
2165
2397
|
if (link === 'AND') {
|
|
2166
2398
|
result = result && nextCond;
|
|
@@ -2174,12 +2406,34 @@
|
|
|
2174
2406
|
}
|
|
2175
2407
|
return result;
|
|
2176
2408
|
}
|
|
2409
|
+
/**
|
|
2410
|
+
* Checks if a PlayerOffer meets its claimable conditions (completed -> claimable transition).
|
|
2411
|
+
* @param claimableConditions - The offer's claimableConditions (from IOffer)
|
|
2412
|
+
* @param claimableTrackers - The player offer's claimableTrackers
|
|
2413
|
+
*/
|
|
2414
|
+
function meetsClaimableConditions({ claimableConditions, playerOfferTrackers, claimableTrackers, }) {
|
|
2415
|
+
if (!claimableConditions) {
|
|
2416
|
+
return { isValid: true };
|
|
2417
|
+
}
|
|
2418
|
+
if (claimableConditions.siblingCompletions) {
|
|
2419
|
+
const siblingCount = playerOfferTrackers?.siblingPlayerOffer_ids?.length ?? 0;
|
|
2420
|
+
let completedCount = claimableTrackers?.siblingCompletions ?? 0;
|
|
2421
|
+
if (completedCount == -1)
|
|
2422
|
+
completedCount = siblingCount; // treat -1 as all completed
|
|
2423
|
+
// if siblings exist but not all are completed, return false
|
|
2424
|
+
if (siblingCount > 0 && completedCount < siblingCount) {
|
|
2425
|
+
return { isValid: false };
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
return { isValid: true };
|
|
2429
|
+
}
|
|
2177
2430
|
|
|
2178
2431
|
const offerListenerEvents = ['claim_offer'];
|
|
2179
2432
|
const PlayerOfferStatuses = [
|
|
2180
2433
|
// 'inQueue', // fuck this shit. just don't surface offers if their offer plate is full.
|
|
2181
2434
|
'surfaced',
|
|
2182
2435
|
'viewed',
|
|
2436
|
+
'completed', // Individual completionConditions met, waiting for claimableConditions (e.g., siblings)
|
|
2183
2437
|
'claimable',
|
|
2184
2438
|
'claimed',
|
|
2185
2439
|
'expired',
|
|
@@ -2228,11 +2482,13 @@
|
|
|
2228
2482
|
exports.SSEConnection = SSEConnection;
|
|
2229
2483
|
exports.hasConditions = hasConditions;
|
|
2230
2484
|
exports.meetsBaseConditions = meetsBaseConditions;
|
|
2485
|
+
exports.meetsClaimableConditions = meetsClaimableConditions;
|
|
2231
2486
|
exports.meetsCompletionConditions = meetsCompletionConditions;
|
|
2232
2487
|
exports.meetsCompletionConditionsBeforeExpiry = meetsCompletionConditionsBeforeExpiry;
|
|
2233
2488
|
exports.meetsDynamicConditions = meetsDynamicConditions;
|
|
2234
2489
|
exports.meetsSurfacingConditions = meetsSurfacingConditions;
|
|
2235
2490
|
exports.offerListenerEvents = offerListenerEvents;
|
|
2491
|
+
exports.offerMeetsCompletionConditions = offerMeetsCompletionConditions;
|
|
2236
2492
|
exports.rewardKinds = rewardKinds;
|
|
2237
2493
|
exports.rewardSchema = rewardSchema;
|
|
2238
2494
|
|