@pixels-online/pixels-client-js-sdk 1.16.0 → 1.18.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.
@@ -1284,19 +1284,47 @@
1284
1284
  }
1285
1285
 
1286
1286
  const keyPattern = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
1287
- // renders template by replacing {keyName} with values from dynamic
1287
+ /**
1288
+ * This replaces {keyName} keys from the template with corresponding values from the dynamic object.
1289
+ */
1288
1290
  function renderTemplate(template, dynamic) {
1289
1291
  if (!template)
1290
1292
  return '';
1291
- return template.replace(keyPattern, (match, key) => {
1293
+ return template.replace(keyPattern, (_match, key) => {
1294
+ if (dynamic && typeof dynamic[key] === 'boolean') {
1295
+ return dynamic[key] ? '✓' : '✗';
1296
+ }
1292
1297
  if (dynamic && dynamic[key] !== undefined) {
1293
1298
  return String(dynamic[key]);
1294
1299
  }
1295
1300
  return '{?}'; // indicate missing key
1296
1301
  });
1297
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
+ }
1298
1322
 
1299
- 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, }) => {
1300
1328
  const conditionData = [];
1301
1329
  let isValid = true;
1302
1330
  if (conditions?.minDaysInGame) {
@@ -1663,7 +1691,11 @@
1663
1691
  }
1664
1692
  // Evaluate dynamic conditions
1665
1693
  if (conditions?.dynamic?.conditions?.length) {
1666
- const dynamicResult = meetsDynamicConditions(playerSnap.dynamic || {}, conditions.dynamic);
1694
+ const resolvedConditions = replaceDynamicConditionKeys(conditions.dynamic.conditions, playerOffer?.trackers || {});
1695
+ const dynamicResult = meetsDynamicConditions(playerSnap.dynamic, {
1696
+ ...conditions.dynamic,
1697
+ conditions: resolvedConditions,
1698
+ });
1667
1699
  if (addDetails) {
1668
1700
  conditionData.push({
1669
1701
  isMet: dynamicResult,
@@ -1772,6 +1804,31 @@
1772
1804
  return { isValid: false };
1773
1805
  }
1774
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
+ }
1775
1832
  return meetsBaseConditions({ conditions, playerSnap });
1776
1833
  };
1777
1834
  const hasConditions = (conditions) => {
@@ -1824,6 +1881,12 @@
1824
1881
  return true;
1825
1882
  if (surCond.links && Object.keys(surCond.links).length > 0)
1826
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;
1827
1890
  const compCond = conditions;
1828
1891
  if (compCond.context)
1829
1892
  return true;
@@ -1845,11 +1908,25 @@
1845
1908
  return true;
1846
1909
  return false;
1847
1910
  };
1848
- const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, addDetails = false, }) => {
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, }) => {
1849
1921
  if (completionConditions) {
1850
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;
1851
1926
  const conditionData = [];
1852
1927
  let isValid = true;
1928
+ let maxTotalClaimsFromScaling = Infinity;
1929
+ const updateMax = (limit) => (maxTotalClaimsFromScaling = Math.min(maxTotalClaimsFromScaling, limit));
1853
1930
  if (completionConditions?.context?.id) {
1854
1931
  const hasTrackedContext = completionTrackers?.context &&
1855
1932
  completionConditions.context.id === completionTrackers.context;
@@ -1866,73 +1943,93 @@
1866
1943
  }
1867
1944
  else {
1868
1945
  if (isDisqualify)
1869
- return { isValid: false };
1946
+ return { isValid: false, availableClaimsNow: 0 };
1870
1947
  }
1871
1948
  }
1872
1949
  if (conditions?.buyItem) {
1873
- const isDisqualify = (completionTrackers?.buyItem || 0) < (conditions.buyItem.amount || 1);
1950
+ const baseAmount = conditions.buyItem.amount || 1;
1951
+ const scaledAmount = baseAmount * claimMultiplier;
1952
+ const trackerValue = completionTrackers?.buyItem || 0;
1953
+ const isDisqualify = trackerValue < scaledAmount;
1954
+ if (shouldScale && baseAmount > 0) {
1955
+ updateMax(Math.floor(trackerValue / baseAmount));
1956
+ }
1874
1957
  if (addDetails) {
1875
1958
  conditionData.push({
1876
1959
  isMet: !isDisqualify,
1877
1960
  kind: 'buyItem',
1878
- trackerAmount: completionTrackers?.buyItem || 0,
1879
- text: `Buy ${conditions.buyItem.amount || 1} ${conditions.buyItem.name}`,
1961
+ trackerAmount: trackerValue,
1962
+ text: `Buy ${scaledAmount} ${conditions.buyItem.name}`,
1880
1963
  });
1881
1964
  if (isDisqualify)
1882
1965
  isValid = false;
1883
1966
  }
1884
1967
  else {
1885
1968
  if (isDisqualify)
1886
- return { isValid: false };
1969
+ return { isValid: false, availableClaimsNow: 0 };
1887
1970
  }
1888
1971
  }
1889
1972
  if (conditions?.spendCurrency) {
1890
- const isDisqualify = (completionTrackers?.spendCurrency || 0) < (conditions.spendCurrency.amount || 1);
1973
+ const baseAmount = conditions.spendCurrency.amount || 1;
1974
+ const scaledAmount = baseAmount * claimMultiplier;
1975
+ const trackerValue = completionTrackers?.spendCurrency || 0;
1976
+ const isDisqualify = trackerValue < scaledAmount;
1977
+ if (shouldScale && baseAmount > 0) {
1978
+ updateMax(Math.floor(trackerValue / baseAmount));
1979
+ }
1891
1980
  if (addDetails) {
1892
1981
  conditionData.push({
1893
1982
  isMet: !isDisqualify,
1894
1983
  kind: 'spendCurrency',
1895
- trackerAmount: completionTrackers?.spendCurrency || 0,
1896
- text: `Spend ${conditions.spendCurrency.amount || 1} ${conditions.spendCurrency.name}`,
1984
+ trackerAmount: trackerValue,
1985
+ text: `Spend ${scaledAmount} ${conditions.spendCurrency.name}`,
1897
1986
  });
1898
1987
  if (isDisqualify)
1899
1988
  isValid = false;
1900
1989
  }
1901
1990
  else {
1902
1991
  if (isDisqualify)
1903
- return { isValid: false };
1992
+ return { isValid: false, availableClaimsNow: 0 };
1904
1993
  }
1905
1994
  }
1906
1995
  if (conditions?.depositCurrency) {
1907
- const isDisqualify = (completionTrackers?.depositCurrency || 0) <
1908
- (conditions.depositCurrency.amount || 1);
1996
+ const baseAmount = conditions.depositCurrency.amount || 1;
1997
+ const scaledAmount = baseAmount * claimMultiplier;
1998
+ const trackerValue = completionTrackers?.depositCurrency || 0;
1999
+ const isDisqualify = trackerValue < scaledAmount;
2000
+ if (shouldScale && baseAmount > 0) {
2001
+ updateMax(Math.floor(trackerValue / baseAmount));
2002
+ }
1909
2003
  if (addDetails) {
1910
2004
  conditionData.push({
1911
2005
  isMet: !isDisqualify,
1912
2006
  kind: 'depositCurrency',
1913
- trackerAmount: completionTrackers?.depositCurrency || 0,
1914
- text: `Deposit ${conditions.depositCurrency.amount || 1} ${conditions.depositCurrency.name}`,
2007
+ trackerAmount: trackerValue,
2008
+ text: `Deposit ${scaledAmount} ${conditions.depositCurrency.name}`,
1915
2009
  });
2010
+ if (isDisqualify)
2011
+ isValid = false;
1916
2012
  }
1917
2013
  else {
1918
2014
  if (isDisqualify)
1919
- return { isValid: false };
2015
+ return { isValid: false, availableClaimsNow: 0 };
1920
2016
  }
1921
2017
  }
1922
2018
  if (conditions?.login) {
1923
- const isMet = completionTrackers?.login || false;
2019
+ const isMet = new Date(playerSnap.snapshotLastUpdated || 0).getTime() >
2020
+ new Date(playerOffer.createdAt || 0).getTime();
1924
2021
  if (addDetails) {
1925
2022
  conditionData.push({
1926
2023
  isMet,
1927
2024
  kind: 'login',
1928
- trackerAmount: completionTrackers?.login ? 1 : 0,
2025
+ trackerAmount: isMet ? 1 : 0,
1929
2026
  text: `Login to the game`,
1930
2027
  });
1931
2028
  isValid = isMet;
1932
2029
  }
1933
2030
  else {
1934
2031
  if (!isMet)
1935
- return { isValid: false };
2032
+ return { isValid: false, availableClaimsNow: 0 };
1936
2033
  }
1937
2034
  }
1938
2035
  if (conditions?.loginStreak) {
@@ -1952,7 +2049,7 @@
1952
2049
  }
1953
2050
  else {
1954
2051
  if (isDisqualify)
1955
- return { isValid: false };
2052
+ return { isValid: false, availableClaimsNow: 0 };
1956
2053
  }
1957
2054
  }
1958
2055
  if (conditions?.social) {
@@ -1962,9 +2059,11 @@
1962
2059
  const hasContent = Boolean(mode === 'accumulate'
1963
2060
  ? tSocial?.mode === 'accumulate'
1964
2061
  : tSocial && tSocial.mode !== 'accumulate' && !!tSocial.videoId);
1965
- const minLikes = cSocial?.minLikes || 0;
1966
- const minViews = cSocial?.minViews || 0;
1967
- const minComments = cSocial?.minComments || 0;
2062
+ // Only scale social metrics in accumulate mode (attach mode is single content)
2063
+ const socialMultiplier = mode === 'accumulate' ? claimMultiplier : 1;
2064
+ const minLikes = (cSocial?.minLikes || 0) * socialMultiplier;
2065
+ const minViews = (cSocial?.minViews || 0) * socialMultiplier;
2066
+ const minComments = (cSocial?.minComments || 0) * socialMultiplier;
1968
2067
  const likes = tSocial?.likes || 0;
1969
2068
  const views = tSocial?.views || 0;
1970
2069
  const comments = tSocial?.comments || 0;
@@ -1972,6 +2071,17 @@
1972
2071
  if (likes < minLikes || views < minViews || comments < minComments) {
1973
2072
  isDisqualify = true;
1974
2073
  }
2074
+ if (shouldScale && mode === 'accumulate' && hasContent) {
2075
+ const baseLikes = cSocial?.minLikes || 0;
2076
+ const baseViews = cSocial?.minViews || 0;
2077
+ const baseComments = cSocial?.minComments || 0;
2078
+ if (baseLikes > 0)
2079
+ updateMax(Math.floor(likes / baseLikes));
2080
+ if (baseViews > 0)
2081
+ updateMax(Math.floor(views / baseViews));
2082
+ if (baseComments > 0)
2083
+ updateMax(Math.floor(comments / baseComments));
2084
+ }
1975
2085
  if (addDetails) {
1976
2086
  const platformMap = {
1977
2087
  tiktok: 'TikTok',
@@ -2043,31 +2153,47 @@
2043
2153
  }
2044
2154
  else {
2045
2155
  if (isDisqualify)
2046
- return { isValid: false };
2156
+ return { isValid: false, availableClaimsNow: 0 };
2047
2157
  }
2048
2158
  }
2049
2159
  // Linked completions - wait for N linked entities to complete
2050
2160
  if (conditions?.linkedCompletions?.min) {
2161
+ const baseMin = conditions.linkedCompletions.min;
2051
2162
  const currentCount = completionTrackers?.linkedCompletions || 0;
2052
- const requiredCount = conditions.linkedCompletions.min;
2053
- const isDisqualify = currentCount < requiredCount;
2163
+ const scaledMin = baseMin * claimMultiplier;
2164
+ const isDisqualify = currentCount < scaledMin;
2165
+ if (shouldScale && baseMin > 0) {
2166
+ updateMax(Math.floor(currentCount / baseMin));
2167
+ }
2054
2168
  if (addDetails) {
2055
2169
  conditionData.push({
2056
2170
  isMet: !isDisqualify,
2057
2171
  kind: 'linkedCompletions',
2058
2172
  trackerAmount: currentCount,
2059
- text: `Wait for ${requiredCount} linked ${requiredCount === 1 ? 'entity' : 'entities'} to complete`,
2173
+ text: `Wait for ${scaledMin} linked ${scaledMin === 1 ? 'entity' : 'entities'} to complete`,
2060
2174
  });
2061
2175
  if (isDisqualify)
2062
2176
  isValid = false;
2063
2177
  }
2064
2178
  else {
2065
2179
  if (isDisqualify)
2066
- return { isValid: false };
2180
+ return { isValid: false, availableClaimsNow: 0 };
2067
2181
  }
2068
2182
  }
2069
2183
  if (conditions?.dynamicTracker?.conditions?.length) {
2070
- const dynamicResult = meetsDynamicConditions(completionTrackers?.dynamicTracker || {}, conditions.dynamicTracker);
2184
+ const resolvedConditions = replaceDynamicConditionKeys(conditions.dynamicTracker.conditions, playerOffer?.trackers || {});
2185
+ // now we have the game-defined conditions with {{}} keys populated. feed these conditions into evaluator
2186
+ const dynamicResult = meetsDynamicConditions(completionTrackers?.dynamicTracker, {
2187
+ ...conditions.dynamicTracker,
2188
+ conditions: resolvedConditions,
2189
+ }, claimMultiplier);
2190
+ if (shouldScale) {
2191
+ const dynamicMax = getMaxClaimsForDynamicGroup(completionTrackers?.dynamicTracker || {}, {
2192
+ ...conditions.dynamicTracker,
2193
+ conditions: resolvedConditions,
2194
+ }, playerOffer.claimedCount || 0);
2195
+ updateMax(dynamicMax);
2196
+ }
2071
2197
  if (addDetails) {
2072
2198
  conditionData.push({
2073
2199
  isMet: dynamicResult,
@@ -2079,19 +2205,29 @@
2079
2205
  }
2080
2206
  else {
2081
2207
  if (!dynamicResult)
2082
- return { isValid: false };
2208
+ return { isValid: false, availableClaimsNow: 0 };
2083
2209
  }
2084
2210
  }
2085
2211
  const r = meetsBaseConditions({
2086
2212
  conditions,
2087
2213
  playerSnap,
2088
2214
  addDetails: true,
2215
+ playerOffer,
2089
2216
  });
2090
2217
  isValid = isValid && r.isValid;
2091
2218
  conditionData.push(...(r.conditionData || []));
2092
- return { isValid, conditionData };
2093
- }
2094
- return { isValid: true, conditionData: [] };
2219
+ if (maxClaimCount && maxClaimCount > 0) {
2220
+ updateMax(maxClaimCount);
2221
+ }
2222
+ const claimedCount = playerOffer.claimedCount || 0;
2223
+ let availableClaimsNow = !isValid
2224
+ ? 0
2225
+ : maxTotalClaimsFromScaling === Infinity
2226
+ ? -1
2227
+ : Math.max(0, maxTotalClaimsFromScaling - claimedCount);
2228
+ return { isValid, conditionData, availableClaimsNow };
2229
+ }
2230
+ return { isValid: true, conditionData: [], availableClaimsNow: -1 };
2095
2231
  };
2096
2232
  /**
2097
2233
  * Checks if completion conditions were met before a specific expiry time.
@@ -2100,10 +2236,9 @@
2100
2236
  * @param completionConditions - The completion conditions to check
2101
2237
  * @param completionTrackers - The completion trackers (for buyItem, spendCurrency, etc.)
2102
2238
  * @param playerSnap - The player snapshot with field timestamps
2103
- * @param expiryTime - The expiry timestamp in milliseconds
2104
2239
  * @returns true if all conditions were met before expiry, false otherwise
2105
2240
  */
2106
- const meetsCompletionConditionsBeforeExpiry = ({ completionConditions, completionTrackers, playerSnap, expiryTime, }) => {
2241
+ const meetsCompletionConditionsBeforeExpiry = ({ completionConditions, completionTrackers, playerSnap, playerOffer, maxClaimCount, }) => {
2107
2242
  if (!completionConditions)
2108
2243
  return false;
2109
2244
  // Check if there are actually any conditions to evaluate
@@ -2113,10 +2248,15 @@
2113
2248
  const conditionsMet = meetsCompletionConditions({
2114
2249
  completionConditions,
2115
2250
  completionTrackers,
2251
+ playerOffer,
2116
2252
  playerSnap,
2253
+ maxClaimCount,
2117
2254
  });
2118
2255
  if (!conditionsMet.isValid)
2119
2256
  return false;
2257
+ if (!playerOffer.expiresAt)
2258
+ return true;
2259
+ const expiryTime = new Date(playerOffer.expiresAt).getTime();
2120
2260
  const lastSnapshotUpdate = new Date(playerSnap.snapshotLastUpdated).getTime();
2121
2261
  /**
2122
2262
  * Checks if a field was updated after the expiry time.
@@ -2227,37 +2367,55 @@
2227
2367
  * Checks if a dynamic object meets a set of dynamic field conditions.
2228
2368
  * @param dynamicObj - The object with any key and string or number value.
2229
2369
  * @param conditions - Array of conditions to check.
2370
+ * @param claimMultiplier - Multiplier to scale conditions (used for numeric comparisons).
2230
2371
  * @returns true if all conditions are met, false otherwise.
2231
2372
  */
2232
2373
  /**
2233
2374
  * Evaluates a single dynamic condition against the dynamic object.
2234
2375
  */
2235
- function evaluateDynamicCondition(dynamicObj, cond) {
2376
+ function evaluateDynamicCondition(dynamicObj, cond, claimMultiplier = 1) {
2377
+ if (!dynamicObj)
2378
+ return false;
2236
2379
  const val = dynamicObj[cond.key];
2237
2380
  if (val === undefined)
2238
2381
  return false;
2239
2382
  const isNumber = typeof val === 'number';
2240
- const compareTo = isNumber ? Number(cond.compareTo) : String(cond.compareTo);
2241
- switch (cond.operator) {
2242
- case '==':
2243
- return val === compareTo;
2244
- case '!=':
2245
- return val !== compareTo;
2383
+ const isBoolean = typeof val === 'boolean';
2384
+ if (isBoolean) {
2385
+ switch (cond.operator) {
2386
+ case '==':
2387
+ return val === Boolean(cond.compareTo);
2388
+ case '!=':
2389
+ return val !== Boolean(cond.compareTo);
2390
+ default:
2391
+ return false;
2392
+ }
2246
2393
  }
2394
+ const compareTo = isNumber ? Number(cond.compareTo) : String(cond.compareTo);
2247
2395
  if (isNumber && typeof compareTo === 'number') {
2396
+ const skipMultiplier = cond.operator === '==' || cond.operator === '!=';
2397
+ const scaledCompareTo = skipMultiplier ? compareTo : compareTo * claimMultiplier;
2248
2398
  switch (cond.operator) {
2399
+ case '==':
2400
+ return val === scaledCompareTo;
2401
+ case '!=':
2402
+ return val !== scaledCompareTo;
2249
2403
  case '>':
2250
- return val > compareTo;
2404
+ return val > scaledCompareTo;
2251
2405
  case '>=':
2252
- return val >= compareTo;
2406
+ return val >= scaledCompareTo;
2253
2407
  case '<':
2254
- return val < compareTo;
2408
+ return val < scaledCompareTo;
2255
2409
  case '<=':
2256
- return val <= compareTo;
2410
+ return val <= scaledCompareTo;
2257
2411
  }
2258
2412
  }
2259
2413
  else if (!isNumber && typeof compareTo === 'string') {
2260
2414
  switch (cond.operator) {
2415
+ case '==':
2416
+ return val === compareTo;
2417
+ case '!=':
2418
+ return val !== compareTo;
2261
2419
  case 'has':
2262
2420
  return val.includes(compareTo);
2263
2421
  case 'not_has':
@@ -2266,24 +2424,119 @@
2266
2424
  }
2267
2425
  return false;
2268
2426
  }
2427
+ /**
2428
+ * Calculates the maximum number of claims supported by a single dynamic condition.
2429
+ */
2430
+ function getMaxClaimsForDynamicCondition(dynamicObj, cond) {
2431
+ if (!dynamicObj)
2432
+ return 0;
2433
+ const val = dynamicObj[cond.key];
2434
+ if (val === undefined)
2435
+ return 0;
2436
+ if (typeof val === 'number') {
2437
+ const base = Number(cond.compareTo);
2438
+ if (isNaN(base)) {
2439
+ return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
2440
+ }
2441
+ switch (cond.operator) {
2442
+ case '>=':
2443
+ if (base === 0)
2444
+ return val >= 0 ? Infinity : 0;
2445
+ if (base < 0)
2446
+ return val >= base ? Infinity : 0;
2447
+ return Math.max(0, Math.floor(val / base));
2448
+ case '>':
2449
+ if (base === 0)
2450
+ return val > 0 ? Infinity : 0;
2451
+ if (base < 0)
2452
+ return val > base ? Infinity : 0;
2453
+ if (val <= 0)
2454
+ return 0;
2455
+ return Math.max(0, Math.ceil(val / base) - 1);
2456
+ case '==':
2457
+ return val === base ? Infinity : 0;
2458
+ case '!=':
2459
+ return val !== base ? Infinity : 0;
2460
+ case '<=':
2461
+ if (base === 0)
2462
+ return val <= 0 ? Infinity : 0;
2463
+ if (base > 0)
2464
+ return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
2465
+ if (val >= 0)
2466
+ return 0;
2467
+ return Math.max(0, Math.floor(val / base));
2468
+ case '<':
2469
+ if (base === 0)
2470
+ return val < 0 ? Infinity : 0;
2471
+ if (base > 0)
2472
+ return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
2473
+ if (val >= 0)
2474
+ return 0;
2475
+ return Math.max(0, Math.ceil(val / base) - 1);
2476
+ }
2477
+ }
2478
+ // we don't scale the rest, they are always true or always false
2479
+ return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
2480
+ }
2481
+ /**
2482
+ * Calculates the maximum number of claims supported by a group of dynamic conditions.
2483
+ */
2484
+ function getMaxClaimsForDynamicGroup(dynamicObj, dynamicGroup, currentClaimCount = 0) {
2485
+ const { conditions, links } = dynamicGroup;
2486
+ if (!conditions || conditions.length === 0)
2487
+ return Infinity;
2488
+ // AND only
2489
+ if (!links || links.length === 0 || links.every((l) => l === 'AND')) {
2490
+ let minClaims = Infinity;
2491
+ for (const cond of conditions) {
2492
+ const max = getMaxClaimsForDynamicCondition(dynamicObj, cond);
2493
+ if (max === 0)
2494
+ return 0;
2495
+ minClaims = Math.min(minClaims, max);
2496
+ }
2497
+ return minClaims;
2498
+ }
2499
+ // OR only
2500
+ if (links.every((l) => l === 'OR')) {
2501
+ let maxClaims = 0;
2502
+ for (const cond of conditions) {
2503
+ const max = getMaxClaimsForDynamicCondition(dynamicObj, cond);
2504
+ if (max === Infinity)
2505
+ return Infinity;
2506
+ maxClaims = Math.max(maxClaims, max);
2507
+ }
2508
+ return maxClaims;
2509
+ }
2510
+ // mixed:
2511
+ const maxIterations = 100;
2512
+ for (let n = currentClaimCount + 1; n <= currentClaimCount + maxIterations; n++) {
2513
+ if (!meetsDynamicConditions(dynamicObj, dynamicGroup, n)) {
2514
+ return n - 1;
2515
+ }
2516
+ }
2517
+ return currentClaimCount + maxIterations;
2518
+ }
2269
2519
  /**
2270
2520
  * Evaluates a group of dynamic conditions with logical links (AND, OR, AND NOT).
2271
2521
  * @param dynamicObj - The player's dynamic object with any key and string or number value.
2272
2522
  * @param dynamicGroup - The group of conditions and links to check.
2523
+ * @param claimMultiplier - Multiplier to scale conditions (used for numeric comparisons).
2273
2524
  * @returns true if the group evaluates to true, false otherwise.
2274
2525
  */
2275
- function meetsDynamicConditions(dynamicObj, dynamicGroup) {
2526
+ function meetsDynamicConditions(dynamicObj, dynamicGroup, claimMultiplier = 1) {
2276
2527
  const { conditions, links } = dynamicGroup;
2277
2528
  if (!conditions || conditions.length === 0)
2278
2529
  return true;
2530
+ if (!dynamicObj)
2531
+ return false;
2279
2532
  // If no links, treat as AND between all conditions
2280
2533
  if (!links || links.length === 0) {
2281
- return conditions.every((cond) => evaluateDynamicCondition(dynamicObj, cond));
2534
+ return conditions.every((cond) => evaluateDynamicCondition(dynamicObj, cond, claimMultiplier));
2282
2535
  }
2283
2536
  // Evaluate the first condition
2284
- let result = evaluateDynamicCondition(dynamicObj, conditions[0]);
2537
+ let result = evaluateDynamicCondition(dynamicObj, conditions[0], claimMultiplier);
2285
2538
  for (let i = 0; i < links.length; i++) {
2286
- const nextCond = evaluateDynamicCondition(dynamicObj, conditions[i + 1]);
2539
+ const nextCond = evaluateDynamicCondition(dynamicObj, conditions[i + 1], claimMultiplier);
2287
2540
  const link = links[i];
2288
2541
  if (link === 'AND') {
2289
2542
  result = result && nextCond;
@@ -2371,6 +2624,8 @@
2371
2624
  exports.OfferwallClient = OfferwallClient;
2372
2625
  exports.PlayerOfferStatuses = PlayerOfferStatuses;
2373
2626
  exports.SSEConnection = SSEConnection;
2627
+ exports.getMaxClaimsForDynamicCondition = getMaxClaimsForDynamicCondition;
2628
+ exports.getMaxClaimsForDynamicGroup = getMaxClaimsForDynamicGroup;
2374
2629
  exports.hasConditions = hasConditions;
2375
2630
  exports.meetsBaseConditions = meetsBaseConditions;
2376
2631
  exports.meetsClaimableConditions = meetsClaimableConditions;
@@ -2379,6 +2634,7 @@
2379
2634
  exports.meetsDynamicConditions = meetsDynamicConditions;
2380
2635
  exports.meetsSurfacingConditions = meetsSurfacingConditions;
2381
2636
  exports.offerListenerEvents = offerListenerEvents;
2637
+ exports.offerMeetsCompletionConditions = offerMeetsCompletionConditions;
2382
2638
  exports.rewardKinds = rewardKinds;
2383
2639
  exports.rewardSchema = rewardSchema;
2384
2640