@stackedapp/utils 1.23.2 → 1.23.4
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/conditions/handlers.d.ts +41 -0
- package/dist/conditions/handlers.js +763 -0
- package/dist/conditions/helpers.d.ts +11 -0
- package/dist/conditions/helpers.js +256 -0
- package/dist/conditions/index.d.ts +97 -0
- package/dist/conditions/index.js +338 -0
- package/dist/dynamic.d.ts +2 -2
- package/dist/template.d.ts +2 -2
- package/package.json +1 -1
- package/dist/conditions.d.ts +0 -130
- package/dist/conditions.js +0 -1746
package/dist/conditions.js
DELETED
|
@@ -1,1746 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.meetsCompletionConditionsBeforeExpiry = exports.meetsCompletionConditions = exports.offerMeetsCompletionConditions = exports.meetsLinkedEntityOffersCondition = exports.hasCompletionConditions = exports.meetsBaseConditions = exports.DEFAULT_ENTITY_KIND = void 0;
|
|
4
|
-
exports.getMaxClaimsForDynamicCondition = getMaxClaimsForDynamicCondition;
|
|
5
|
-
exports.getMaxClaimsForDynamicGroup = getMaxClaimsForDynamicGroup;
|
|
6
|
-
exports.meetsDynamicConditions = meetsDynamicConditions;
|
|
7
|
-
exports.meetsClaimableConditions = meetsClaimableConditions;
|
|
8
|
-
exports.aggregateTokenBalances = aggregateTokenBalances;
|
|
9
|
-
const template_1 = require("./template");
|
|
10
|
-
const dynamic_1 = require("./dynamic");
|
|
11
|
-
const blockchain_utils_1 = require("./blockchain_utils");
|
|
12
|
-
exports.DEFAULT_ENTITY_KIND = '_default';
|
|
13
|
-
const calculatePercent = (amount, goal, isMet) => {
|
|
14
|
-
if (goal && goal > 0 && amount !== undefined) {
|
|
15
|
-
return Math.min(100, (amount / goal) * 100);
|
|
16
|
-
}
|
|
17
|
-
return isMet ? 100 : 0;
|
|
18
|
-
};
|
|
19
|
-
const formatList = (items) => items.length === 2 ? items.join(' and ') : `${items.slice(0, -1).join(', ')}, and ${items.at(-1)}`;
|
|
20
|
-
const calculateDynamicConditionPercent = (dynamicObj, cond) => {
|
|
21
|
-
if (!dynamicObj)
|
|
22
|
-
return 0;
|
|
23
|
-
if (cond.operator === 'is_truthy')
|
|
24
|
-
return dynamicObj[cond.key] ? 100 : 0;
|
|
25
|
-
if (cond.operator === 'is_falsy')
|
|
26
|
-
return !dynamicObj[cond.key] ? 100 : 0;
|
|
27
|
-
if (cond.operator === 'is_defined')
|
|
28
|
-
return dynamicObj[cond.key] != null ? 100 : 0;
|
|
29
|
-
const val = dynamicObj[cond.key];
|
|
30
|
-
if (typeof val !== 'number')
|
|
31
|
-
return 0;
|
|
32
|
-
const compareTo = typeof cond.compareTo === 'number' ? cond.compareTo : 0;
|
|
33
|
-
switch (cond.operator) {
|
|
34
|
-
case '>=':
|
|
35
|
-
case '>':
|
|
36
|
-
return compareTo > 0 ? Math.min(100, (val / compareTo) * 100) : val >= compareTo ? 100 : 0;
|
|
37
|
-
case '==':
|
|
38
|
-
return val === compareTo ? 100 : 0;
|
|
39
|
-
case '!=':
|
|
40
|
-
return val !== compareTo ? 100 : 0;
|
|
41
|
-
case '<=':
|
|
42
|
-
case '<':
|
|
43
|
-
return val <= compareTo ? 100 : 0; // Binary for max-style
|
|
44
|
-
default:
|
|
45
|
-
return 0;
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
const calculateDynamicGroupPercent = (dynamicObj, group) => {
|
|
49
|
-
if (!group?.conditions?.length)
|
|
50
|
-
return 100;
|
|
51
|
-
const percentages = group.conditions.map((c) => calculateDynamicConditionPercent(dynamicObj, c));
|
|
52
|
-
if (!group.links?.length) {
|
|
53
|
-
return percentages.reduce((sum, p) => sum + p, 0) / percentages.length;
|
|
54
|
-
}
|
|
55
|
-
let result = percentages[0];
|
|
56
|
-
for (let i = 0; i < group.links.length; i++) {
|
|
57
|
-
const nextPct = percentages[i + 1];
|
|
58
|
-
switch (group.links[i]) {
|
|
59
|
-
case 'AND':
|
|
60
|
-
result = (result + nextPct) / 2;
|
|
61
|
-
break;
|
|
62
|
-
case 'OR':
|
|
63
|
-
result = Math.max(result, nextPct);
|
|
64
|
-
break;
|
|
65
|
-
case 'AND NOT':
|
|
66
|
-
result = (result + (100 - nextPct)) / 2;
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return result;
|
|
71
|
-
};
|
|
72
|
-
const meetsBaseConditions = ({ conditions, playerSnap, addDetails,
|
|
73
|
-
/** this exists if calling meetsBaseConditions from meetsCompletionConditions. but surfacing
|
|
74
|
-
* check doesn't use this since we don't have a playerOffer at surfacing time
|
|
75
|
-
*/
|
|
76
|
-
playerOffer,
|
|
77
|
-
/** Additional data like fetched token balances that isn't part of playerSnap */
|
|
78
|
-
additionalData, }) => {
|
|
79
|
-
const conditionData = [];
|
|
80
|
-
let isValid = true;
|
|
81
|
-
if (conditions?.minDaysInGame) {
|
|
82
|
-
const trackerAmount = playerSnap.daysInGame || 0;
|
|
83
|
-
const trackerGoal = conditions.minDaysInGame;
|
|
84
|
-
const isDisqualify = trackerAmount < trackerGoal;
|
|
85
|
-
if (addDetails) {
|
|
86
|
-
conditionData.push({
|
|
87
|
-
isMet: !isDisqualify,
|
|
88
|
-
kind: 'minDaysInGame',
|
|
89
|
-
trackerAmount,
|
|
90
|
-
trackerGoal,
|
|
91
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
92
|
-
text: `More than ${trackerGoal} Days in Game`,
|
|
93
|
-
});
|
|
94
|
-
if (isDisqualify)
|
|
95
|
-
isValid = false;
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
if (isDisqualify)
|
|
99
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (conditions?.minTrustScore) {
|
|
103
|
-
const trackerAmount = playerSnap.trustScore || 0;
|
|
104
|
-
const trackerGoal = conditions.minTrustScore;
|
|
105
|
-
const isDisqualify = trackerAmount < trackerGoal;
|
|
106
|
-
if (addDetails) {
|
|
107
|
-
conditionData.push({
|
|
108
|
-
isMet: !isDisqualify,
|
|
109
|
-
kind: 'minTrustScore',
|
|
110
|
-
trackerAmount,
|
|
111
|
-
trackerGoal,
|
|
112
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
113
|
-
text: `More than ${trackerGoal} Rep`,
|
|
114
|
-
});
|
|
115
|
-
if (isDisqualify)
|
|
116
|
-
isValid = false;
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
if (isDisqualify)
|
|
120
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (conditions?.maxTrustScore) {
|
|
124
|
-
const trackerAmount = playerSnap.trustScore || 0;
|
|
125
|
-
const isDisqualify = trackerAmount > conditions.maxTrustScore;
|
|
126
|
-
if (addDetails) {
|
|
127
|
-
conditionData.push({
|
|
128
|
-
isMet: !isDisqualify,
|
|
129
|
-
kind: 'maxTrustScore',
|
|
130
|
-
trackerAmount,
|
|
131
|
-
trackerGoal: conditions.maxTrustScore,
|
|
132
|
-
percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
|
|
133
|
-
text: `Less than ${conditions.maxTrustScore} Rep`,
|
|
134
|
-
});
|
|
135
|
-
if (isDisqualify)
|
|
136
|
-
isValid = false;
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
if (isDisqualify)
|
|
140
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
for (const key in conditions?.achievements) {
|
|
144
|
-
const a = conditions.achievements[key];
|
|
145
|
-
const playerAchData = playerSnap.achievements?.[key];
|
|
146
|
-
if (!playerAchData) {
|
|
147
|
-
const isDisqualify = true;
|
|
148
|
-
if (addDetails) {
|
|
149
|
-
conditionData.push({
|
|
150
|
-
isMet: !isDisqualify,
|
|
151
|
-
kind: 'achievements',
|
|
152
|
-
trackerAmount: 0,
|
|
153
|
-
trackerGoal: 1,
|
|
154
|
-
percentCompleted: 0,
|
|
155
|
-
text: `Have the achievement ${a.name}`,
|
|
156
|
-
});
|
|
157
|
-
if (isDisqualify)
|
|
158
|
-
isValid = false;
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
if (isDisqualify)
|
|
162
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
if (a.minCount) {
|
|
166
|
-
const trackerAmount = playerAchData?.count || 0;
|
|
167
|
-
const trackerGoal = a.minCount;
|
|
168
|
-
const isDisqualify = trackerAmount < trackerGoal;
|
|
169
|
-
if (addDetails) {
|
|
170
|
-
conditionData.push({
|
|
171
|
-
isMet: !isDisqualify,
|
|
172
|
-
kind: 'achievements',
|
|
173
|
-
trackerAmount,
|
|
174
|
-
trackerGoal,
|
|
175
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
176
|
-
text: `Have the achievement ${a.name} more than ${trackerGoal} times`,
|
|
177
|
-
});
|
|
178
|
-
if (isDisqualify)
|
|
179
|
-
isValid = false;
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
if (isDisqualify)
|
|
183
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
for (const key in conditions?.currencies) {
|
|
188
|
-
const c = conditions.currencies[key];
|
|
189
|
-
const playerCurrencyData = playerSnap.currencies?.[key];
|
|
190
|
-
if (c.min) {
|
|
191
|
-
const trackerAmount = playerCurrencyData?.balance || 0;
|
|
192
|
-
const trackerGoal = c.min;
|
|
193
|
-
const isDisqualify = trackerAmount < trackerGoal;
|
|
194
|
-
if (addDetails) {
|
|
195
|
-
conditionData.push({
|
|
196
|
-
isMet: !isDisqualify,
|
|
197
|
-
kind: 'currencies',
|
|
198
|
-
trackerAmount,
|
|
199
|
-
trackerGoal,
|
|
200
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
201
|
-
text: `Have more than ${trackerGoal} ${c.name}`,
|
|
202
|
-
});
|
|
203
|
-
if (isDisqualify)
|
|
204
|
-
isValid = false;
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
if (isDisqualify)
|
|
208
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (c.max) {
|
|
212
|
-
const trackerAmount = playerCurrencyData?.balance || 0;
|
|
213
|
-
const isDisqualify = trackerAmount > c.max;
|
|
214
|
-
if (addDetails) {
|
|
215
|
-
conditionData.push({
|
|
216
|
-
isMet: !isDisqualify,
|
|
217
|
-
kind: 'currencies',
|
|
218
|
-
trackerAmount,
|
|
219
|
-
trackerGoal: c.max,
|
|
220
|
-
percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
|
|
221
|
-
text: `Have less than ${c.max} ${c.name}`,
|
|
222
|
-
});
|
|
223
|
-
if (isDisqualify)
|
|
224
|
-
isValid = false;
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
if (isDisqualify)
|
|
228
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
if (c.in) {
|
|
232
|
-
const trackerAmount = playerCurrencyData?.in || 0;
|
|
233
|
-
const trackerGoal = c.in;
|
|
234
|
-
const isDisqualify = trackerAmount < trackerGoal;
|
|
235
|
-
if (addDetails) {
|
|
236
|
-
conditionData.push({
|
|
237
|
-
isMet: !isDisqualify,
|
|
238
|
-
kind: 'currencies',
|
|
239
|
-
trackerAmount,
|
|
240
|
-
trackerGoal,
|
|
241
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
242
|
-
text: `Deposit at least ${trackerGoal} ${c.name}`,
|
|
243
|
-
});
|
|
244
|
-
if (isDisqualify)
|
|
245
|
-
isValid = false;
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
if (isDisqualify)
|
|
249
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
if (c.out) {
|
|
253
|
-
const trackerAmount = playerCurrencyData?.out || 0;
|
|
254
|
-
const trackerGoal = c.out;
|
|
255
|
-
const isDisqualify = trackerAmount < trackerGoal;
|
|
256
|
-
if (addDetails) {
|
|
257
|
-
conditionData.push({
|
|
258
|
-
isMet: !isDisqualify,
|
|
259
|
-
kind: 'currencies',
|
|
260
|
-
trackerAmount,
|
|
261
|
-
trackerGoal,
|
|
262
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
263
|
-
text: `Withdraw at least ${trackerGoal} ${c.name}`,
|
|
264
|
-
});
|
|
265
|
-
if (isDisqualify)
|
|
266
|
-
isValid = false;
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
if (isDisqualify)
|
|
270
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
for (const key in conditions?.levels) {
|
|
275
|
-
const l = conditions.levels[key];
|
|
276
|
-
const playerLevelData = playerSnap.levels?.[key];
|
|
277
|
-
if (l.min) {
|
|
278
|
-
const trackerAmount = playerLevelData?.level || 0;
|
|
279
|
-
const trackerGoal = l.min;
|
|
280
|
-
const isDisqualify = trackerAmount < trackerGoal;
|
|
281
|
-
if (addDetails) {
|
|
282
|
-
conditionData.push({
|
|
283
|
-
isMet: !isDisqualify,
|
|
284
|
-
kind: 'levels',
|
|
285
|
-
trackerAmount,
|
|
286
|
-
trackerGoal,
|
|
287
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
288
|
-
text: `Be above level ${trackerGoal} ${l.name}`,
|
|
289
|
-
});
|
|
290
|
-
if (isDisqualify)
|
|
291
|
-
isValid = false;
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
if (isDisqualify)
|
|
295
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
if (l.max) {
|
|
299
|
-
const trackerAmount = playerLevelData?.level || 0;
|
|
300
|
-
const isDisqualify = trackerAmount > l.max;
|
|
301
|
-
if (addDetails) {
|
|
302
|
-
conditionData.push({
|
|
303
|
-
isMet: !isDisqualify,
|
|
304
|
-
kind: 'levels',
|
|
305
|
-
trackerAmount,
|
|
306
|
-
trackerGoal: l.max,
|
|
307
|
-
percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
|
|
308
|
-
text: `Be under level ${l.max} ${l.name}`,
|
|
309
|
-
});
|
|
310
|
-
if (isDisqualify)
|
|
311
|
-
isValid = false;
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
if (isDisqualify)
|
|
315
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
if (conditions?.quests) {
|
|
320
|
-
for (const questId in conditions.quests) {
|
|
321
|
-
const quest = conditions.quests[questId];
|
|
322
|
-
const playerQuestData = playerSnap.quests?.[questId];
|
|
323
|
-
const trackerAmount = playerQuestData?.completions || 0;
|
|
324
|
-
const trackerGoal = quest.completions || 1;
|
|
325
|
-
const isDisqualify = playerQuestData ? trackerAmount < (quest.completions || 0) : true;
|
|
326
|
-
if (addDetails) {
|
|
327
|
-
conditionData.push({
|
|
328
|
-
isMet: !isDisqualify,
|
|
329
|
-
kind: 'quests',
|
|
330
|
-
trackerAmount,
|
|
331
|
-
trackerGoal,
|
|
332
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
333
|
-
text: quest.completions === 1
|
|
334
|
-
? `Complete the quest ${quest.name}`
|
|
335
|
-
: (quest.completions || 0) < 1
|
|
336
|
-
? `Start the quest ${quest.name}`
|
|
337
|
-
: `Complete the quest ${quest.name} ${quest.completions} times`,
|
|
338
|
-
});
|
|
339
|
-
if (isDisqualify)
|
|
340
|
-
isValid = false;
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
if (isDisqualify)
|
|
344
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
for (const key in conditions?.memberships) {
|
|
349
|
-
const m = conditions.memberships[key];
|
|
350
|
-
const playerMembershipsData = playerSnap.memberships?.[key];
|
|
351
|
-
if (m.minCount) {
|
|
352
|
-
const trackerAmount = playerMembershipsData?.count || 0;
|
|
353
|
-
const trackerGoal = m.minCount;
|
|
354
|
-
const isDisqualify = trackerAmount < trackerGoal;
|
|
355
|
-
if (addDetails) {
|
|
356
|
-
conditionData.push({
|
|
357
|
-
isMet: !isDisqualify,
|
|
358
|
-
kind: 'memberships',
|
|
359
|
-
trackerAmount,
|
|
360
|
-
trackerGoal,
|
|
361
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
362
|
-
text: trackerGoal > 1 ? `Have at least ${trackerGoal} ${m.name} memberships` : `Have a ${m.name} membership`,
|
|
363
|
-
});
|
|
364
|
-
if (isDisqualify)
|
|
365
|
-
isValid = false;
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
if (isDisqualify)
|
|
369
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
if (m.maxCount) {
|
|
373
|
-
const trackerAmount = playerMembershipsData?.count || 0;
|
|
374
|
-
const isDisqualify = trackerAmount > m.maxCount;
|
|
375
|
-
if (addDetails) {
|
|
376
|
-
conditionData.push({
|
|
377
|
-
isMet: !isDisqualify,
|
|
378
|
-
kind: 'memberships',
|
|
379
|
-
trackerAmount,
|
|
380
|
-
trackerGoal: m.maxCount,
|
|
381
|
-
percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
|
|
382
|
-
text: `Have less than ${m.maxCount} ${m.name} memberships`,
|
|
383
|
-
});
|
|
384
|
-
if (isDisqualify)
|
|
385
|
-
isValid = false;
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
if (isDisqualify)
|
|
389
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
const timeOwned = (playerMembershipsData?.expiresAt || 0) - Date.now();
|
|
393
|
-
if (m.minMs) {
|
|
394
|
-
const trackerAmount = Number((timeOwned / (1000 * 60 * 60 * 24)).toFixed(1));
|
|
395
|
-
const trackerGoal = Number((m.minMs / (1000 * 60 * 60 * 24)).toFixed(1));
|
|
396
|
-
const isDisqualify = timeOwned < m.minMs;
|
|
397
|
-
if (addDetails) {
|
|
398
|
-
conditionData.push({
|
|
399
|
-
isMet: !isDisqualify,
|
|
400
|
-
kind: 'memberships',
|
|
401
|
-
trackerAmount,
|
|
402
|
-
trackerGoal,
|
|
403
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
404
|
-
text: `Own ${m.name} membership for at least ${trackerGoal} days`,
|
|
405
|
-
});
|
|
406
|
-
if (isDisqualify)
|
|
407
|
-
isValid = false;
|
|
408
|
-
}
|
|
409
|
-
else {
|
|
410
|
-
if (isDisqualify)
|
|
411
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
if (m.maxMs) {
|
|
415
|
-
const trackerAmount = Number((timeOwned / (1000 * 60 * 60 * 24)).toFixed(1));
|
|
416
|
-
const isDisqualify = timeOwned > m.maxMs;
|
|
417
|
-
if (addDetails) {
|
|
418
|
-
conditionData.push({
|
|
419
|
-
isMet: !isDisqualify,
|
|
420
|
-
kind: 'memberships',
|
|
421
|
-
trackerAmount,
|
|
422
|
-
trackerGoal: Number((m.maxMs / (1000 * 60 * 60 * 24)).toFixed(1)),
|
|
423
|
-
percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
|
|
424
|
-
text: `Own ${m.name} membership for less than ${(m.maxMs / (1000 * 60 * 60 * 24)).toFixed(1)} days`,
|
|
425
|
-
});
|
|
426
|
-
if (isDisqualify)
|
|
427
|
-
isValid = false;
|
|
428
|
-
}
|
|
429
|
-
else {
|
|
430
|
-
if (isDisqualify)
|
|
431
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
for (const key in conditions?.stakedTokens) {
|
|
436
|
-
const s = conditions.stakedTokens[key];
|
|
437
|
-
const playerStakedData = playerSnap.stakedTokens?.[key];
|
|
438
|
-
if (s.min) {
|
|
439
|
-
const trackerAmount = playerStakedData?.balance || 0;
|
|
440
|
-
const trackerGoal = s.min;
|
|
441
|
-
const isDisqualify = trackerAmount < trackerGoal;
|
|
442
|
-
if (addDetails) {
|
|
443
|
-
conditionData.push({
|
|
444
|
-
isMet: !isDisqualify,
|
|
445
|
-
kind: 'stakedTokens',
|
|
446
|
-
trackerAmount,
|
|
447
|
-
trackerGoal,
|
|
448
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
449
|
-
text: `Have at least ${trackerGoal} ${s.name} staked`,
|
|
450
|
-
});
|
|
451
|
-
if (isDisqualify)
|
|
452
|
-
isValid = false;
|
|
453
|
-
}
|
|
454
|
-
else {
|
|
455
|
-
if (isDisqualify)
|
|
456
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
// Validate link count conditions
|
|
461
|
-
if (conditions?.links) {
|
|
462
|
-
for (const [linkType, constraint] of Object.entries(conditions.links)) {
|
|
463
|
-
// linkType should always exist. and be default is none was specified
|
|
464
|
-
const linkCount = playerSnap.entityLinks?.filter((link) => (link.kind || exports.DEFAULT_ENTITY_KIND) === linkType).length || 0;
|
|
465
|
-
if (constraint.min !== undefined) {
|
|
466
|
-
const trackerAmount = linkCount;
|
|
467
|
-
const trackerGoal = constraint.min;
|
|
468
|
-
const isDisqualify = trackerAmount < trackerGoal;
|
|
469
|
-
if (addDetails) {
|
|
470
|
-
conditionData.push({
|
|
471
|
-
isMet: !isDisqualify,
|
|
472
|
-
kind: 'links',
|
|
473
|
-
trackerAmount,
|
|
474
|
-
trackerGoal,
|
|
475
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
476
|
-
text: constraint.template
|
|
477
|
-
? (0, template_1.renderTemplate)(constraint.template, {
|
|
478
|
-
current: linkCount,
|
|
479
|
-
min: constraint.min ?? 0,
|
|
480
|
-
max: constraint.max ?? 0,
|
|
481
|
-
type: linkType,
|
|
482
|
-
})
|
|
483
|
-
: `At least ${trackerGoal} ${linkType} link(s)`,
|
|
484
|
-
});
|
|
485
|
-
if (isDisqualify)
|
|
486
|
-
isValid = false;
|
|
487
|
-
}
|
|
488
|
-
else {
|
|
489
|
-
if (isDisqualify)
|
|
490
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
if (constraint.max !== undefined) {
|
|
494
|
-
const trackerAmount = linkCount;
|
|
495
|
-
const isDisqualify = trackerAmount > constraint.max;
|
|
496
|
-
if (addDetails) {
|
|
497
|
-
conditionData.push({
|
|
498
|
-
isMet: !isDisqualify,
|
|
499
|
-
kind: 'links',
|
|
500
|
-
trackerAmount,
|
|
501
|
-
trackerGoal: constraint.max,
|
|
502
|
-
percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
|
|
503
|
-
text: constraint.template
|
|
504
|
-
? (0, template_1.renderTemplate)(constraint.template, {
|
|
505
|
-
current: linkCount,
|
|
506
|
-
min: constraint.min ?? 0,
|
|
507
|
-
max: constraint.max ?? 0,
|
|
508
|
-
type: linkType,
|
|
509
|
-
})
|
|
510
|
-
: `At most ${constraint.max} ${linkType} link(s)`,
|
|
511
|
-
});
|
|
512
|
-
if (isDisqualify)
|
|
513
|
-
isValid = false;
|
|
514
|
-
}
|
|
515
|
-
else {
|
|
516
|
-
if (isDisqualify)
|
|
517
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
// Evaluate dynamic conditions
|
|
523
|
-
if (conditions?.dynamic?.conditions?.length) {
|
|
524
|
-
const resolvedConditions = (0, template_1.replaceDynamicConditionKeys)(conditions.dynamic.conditions, playerOffer?.trackers || {});
|
|
525
|
-
const dynamicObj = playerSnap.dynamic || {};
|
|
526
|
-
const dynamicResult = meetsDynamicConditions(dynamicObj, {
|
|
527
|
-
...conditions.dynamic,
|
|
528
|
-
conditions: resolvedConditions,
|
|
529
|
-
});
|
|
530
|
-
if (addDetails) {
|
|
531
|
-
// Extract tracker amount and goal from the first condition for display
|
|
532
|
-
let trackerAmount;
|
|
533
|
-
let trackerGoal;
|
|
534
|
-
if (resolvedConditions.length > 0) {
|
|
535
|
-
const firstCond = resolvedConditions[0];
|
|
536
|
-
if (typeof firstCond.compareTo === 'number') {
|
|
537
|
-
trackerGoal = firstCond.compareTo;
|
|
538
|
-
}
|
|
539
|
-
const val = dynamicObj[firstCond.key];
|
|
540
|
-
trackerAmount = typeof val === 'number' ? val : 0;
|
|
541
|
-
}
|
|
542
|
-
// Calculate percentage using the dynamic group helper for AND/OR logic
|
|
543
|
-
const percentCompleted = calculateDynamicGroupPercent(dynamicObj, {
|
|
544
|
-
...conditions.dynamic,
|
|
545
|
-
conditions: resolvedConditions,
|
|
546
|
-
});
|
|
547
|
-
conditionData.push({
|
|
548
|
-
isMet: dynamicResult,
|
|
549
|
-
kind: 'dynamic',
|
|
550
|
-
trackerAmount,
|
|
551
|
-
trackerGoal,
|
|
552
|
-
percentCompleted,
|
|
553
|
-
text: (0, template_1.renderTemplate)((0, template_1.replaceDynamicConditionKey)(conditions.dynamic.template || '', playerOffer?.trackers || {}), dynamicObj) || 'Dynamic conditions',
|
|
554
|
-
});
|
|
555
|
-
if (!dynamicResult)
|
|
556
|
-
isValid = false;
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
if (!dynamicResult)
|
|
560
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
if (conditions?.identifiers?.platforms?.length) {
|
|
564
|
-
const playerPlatforms = new Set(playerSnap.identifiers?.map((i) => i.platform.toLowerCase()) || []);
|
|
565
|
-
const isAndBehaviour = conditions.identifiers.behaviour === 'AND';
|
|
566
|
-
const platformsToCheck = conditions.identifiers.platforms;
|
|
567
|
-
let isMet;
|
|
568
|
-
let displayText;
|
|
569
|
-
const platformsMap = {
|
|
570
|
-
sms: 'SMS',
|
|
571
|
-
email: 'Email',
|
|
572
|
-
apple: 'Apple',
|
|
573
|
-
google: 'Google',
|
|
574
|
-
tiktok: 'TikTok',
|
|
575
|
-
};
|
|
576
|
-
const metAndPlatforms = platformsToCheck.filter((platform) => playerPlatforms.has(platform.toLowerCase()));
|
|
577
|
-
if (isAndBehaviour) {
|
|
578
|
-
isMet = metAndPlatforms.length === platformsToCheck.length;
|
|
579
|
-
displayText =
|
|
580
|
-
platformsToCheck.length > 1
|
|
581
|
-
? `Link your ${formatList(platformsToCheck.map((p) => platformsMap[p.toLowerCase()] ?? p))} accounts`
|
|
582
|
-
: `Link your ${platformsMap[platformsToCheck[0].toLowerCase()] ?? platformsToCheck[0]} account`;
|
|
583
|
-
}
|
|
584
|
-
else {
|
|
585
|
-
isMet = platformsToCheck.some((platform) => playerPlatforms.has(platform.toLowerCase()));
|
|
586
|
-
displayText = `Link any of your ${formatList(platformsToCheck.map((p) => platformsMap[p.toLowerCase()] ?? p))} accounts`;
|
|
587
|
-
}
|
|
588
|
-
if (addDetails) {
|
|
589
|
-
conditionData.push({
|
|
590
|
-
isMet,
|
|
591
|
-
kind: 'identifiers',
|
|
592
|
-
trackerAmount: isMet ? 1 : 0,
|
|
593
|
-
trackerGoal: 1,
|
|
594
|
-
percentCompleted: isAndBehaviour
|
|
595
|
-
? calculatePercent(metAndPlatforms.length, platformsToCheck.length, isMet)
|
|
596
|
-
: isMet
|
|
597
|
-
? 100
|
|
598
|
-
: 0,
|
|
599
|
-
text: displayText,
|
|
600
|
-
});
|
|
601
|
-
if (!isMet)
|
|
602
|
-
isValid = false;
|
|
603
|
-
}
|
|
604
|
-
else {
|
|
605
|
-
if (!isMet)
|
|
606
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
// Evaluate token balance conditions
|
|
610
|
-
for (const tokenCond of conditions?.tokenBalances || []) {
|
|
611
|
-
const contracts = tokenCond.contracts || [];
|
|
612
|
-
let totalBalance = 0;
|
|
613
|
-
const fetchedBalances = aggregateTokenBalances(additionalData);
|
|
614
|
-
for (const contract of contracts) {
|
|
615
|
-
const balanceKey = (0, blockchain_utils_1.addressNetworkId)(contract.contractAddress, contract.network);
|
|
616
|
-
totalBalance += fetchedBalances[balanceKey] || 0;
|
|
617
|
-
}
|
|
618
|
-
if (tokenCond.min !== undefined) {
|
|
619
|
-
const trackerAmount = totalBalance;
|
|
620
|
-
const trackerGoal = tokenCond.min;
|
|
621
|
-
const isDisqualify = trackerAmount < trackerGoal;
|
|
622
|
-
if (addDetails) {
|
|
623
|
-
conditionData.push({
|
|
624
|
-
isMet: !isDisqualify,
|
|
625
|
-
kind: 'tokenBalances',
|
|
626
|
-
trackerAmount,
|
|
627
|
-
trackerGoal,
|
|
628
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
629
|
-
text: `Have at least ${trackerGoal} ${tokenCond.name || 'tokens'}`,
|
|
630
|
-
});
|
|
631
|
-
if (isDisqualify)
|
|
632
|
-
isValid = false;
|
|
633
|
-
}
|
|
634
|
-
else {
|
|
635
|
-
if (isDisqualify)
|
|
636
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
if (tokenCond.max !== undefined) {
|
|
640
|
-
const trackerAmount = totalBalance;
|
|
641
|
-
const isDisqualify = trackerAmount > tokenCond.max;
|
|
642
|
-
if (addDetails) {
|
|
643
|
-
conditionData.push({
|
|
644
|
-
isMet: !isDisqualify,
|
|
645
|
-
kind: 'tokenBalances',
|
|
646
|
-
trackerAmount,
|
|
647
|
-
trackerGoal: tokenCond.max,
|
|
648
|
-
percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
|
|
649
|
-
text: `Have at most ${tokenCond.max} ${tokenCond.name || 'tokens'}`,
|
|
650
|
-
});
|
|
651
|
-
if (isDisqualify)
|
|
652
|
-
isValid = false;
|
|
653
|
-
}
|
|
654
|
-
else {
|
|
655
|
-
if (isDisqualify)
|
|
656
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
// Evaluate stackedAccount conditions
|
|
661
|
-
if (conditions?.stackedAccount) {
|
|
662
|
-
const { hasLinkedAccount, cryptoWallets } = conditions.stackedAccount;
|
|
663
|
-
// Check if player has linked stacked account
|
|
664
|
-
if (hasLinkedAccount !== undefined) {
|
|
665
|
-
const playerHasAccount = additionalData?.hasStackedAccount ?? false;
|
|
666
|
-
const isDisqualify = hasLinkedAccount !== playerHasAccount;
|
|
667
|
-
if (addDetails) {
|
|
668
|
-
conditionData.push({
|
|
669
|
-
isMet: !isDisqualify,
|
|
670
|
-
kind: 'stackedAccount.hasLinkedAccount',
|
|
671
|
-
trackerAmount: playerHasAccount ? 1 : 0,
|
|
672
|
-
trackerGoal: 1,
|
|
673
|
-
percentCompleted: !isDisqualify ? 100 : 0,
|
|
674
|
-
text: hasLinkedAccount ? 'Link a Stacked account' : 'Must not have Stacked account linked',
|
|
675
|
-
});
|
|
676
|
-
if (isDisqualify)
|
|
677
|
-
isValid = false;
|
|
678
|
-
}
|
|
679
|
-
else {
|
|
680
|
-
if (isDisqualify)
|
|
681
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
// Check crypto wallet count
|
|
685
|
-
if (cryptoWallets) {
|
|
686
|
-
const walletCount = additionalData?.cryptoWallets?.length || 0;
|
|
687
|
-
if (cryptoWallets.min !== undefined) {
|
|
688
|
-
const trackerAmount = walletCount;
|
|
689
|
-
const trackerGoal = cryptoWallets.min;
|
|
690
|
-
const isDisqualify = trackerAmount < trackerGoal;
|
|
691
|
-
if (addDetails) {
|
|
692
|
-
conditionData.push({
|
|
693
|
-
isMet: !isDisqualify,
|
|
694
|
-
kind: 'stackedAccount.cryptoWallets',
|
|
695
|
-
trackerAmount,
|
|
696
|
-
trackerGoal,
|
|
697
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
698
|
-
text: `Link at least ${trackerGoal} crypto wallet${trackerGoal === 1 ? '' : 's'}`,
|
|
699
|
-
});
|
|
700
|
-
if (isDisqualify)
|
|
701
|
-
isValid = false;
|
|
702
|
-
}
|
|
703
|
-
else {
|
|
704
|
-
if (isDisqualify)
|
|
705
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
if (cryptoWallets.max !== undefined) {
|
|
709
|
-
const trackerAmount = walletCount;
|
|
710
|
-
const trackerGoal = cryptoWallets.max;
|
|
711
|
-
const isDisqualify = trackerAmount > trackerGoal;
|
|
712
|
-
if (addDetails) {
|
|
713
|
-
conditionData.push({
|
|
714
|
-
isMet: !isDisqualify,
|
|
715
|
-
kind: 'stackedAccount.cryptoWallets',
|
|
716
|
-
trackerAmount,
|
|
717
|
-
trackerGoal,
|
|
718
|
-
percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
|
|
719
|
-
text: `Have at most ${trackerGoal} crypto wallet${trackerGoal === 1 ? '' : 's'}`,
|
|
720
|
-
});
|
|
721
|
-
if (isDisqualify)
|
|
722
|
-
isValid = false;
|
|
723
|
-
}
|
|
724
|
-
else {
|
|
725
|
-
if (isDisqualify)
|
|
726
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
// Evaluate userSettings conditions
|
|
732
|
-
if (conditions?.userSettings) {
|
|
733
|
-
const { emailNewsletter } = conditions.userSettings;
|
|
734
|
-
if (emailNewsletter !== undefined) {
|
|
735
|
-
const playerHasNewsletter = additionalData?.emailNewsletter ?? false;
|
|
736
|
-
const isDisqualify = emailNewsletter !== playerHasNewsletter;
|
|
737
|
-
if (addDetails) {
|
|
738
|
-
conditionData.push({
|
|
739
|
-
isMet: !isDisqualify,
|
|
740
|
-
kind: 'userSettings.emailNewsletter',
|
|
741
|
-
trackerAmount: playerHasNewsletter ? 1 : 0,
|
|
742
|
-
trackerGoal: 1,
|
|
743
|
-
percentCompleted: !isDisqualify ? 100 : 0,
|
|
744
|
-
text: emailNewsletter ? 'Subscribe to email newsletter' : 'Must not be subscribed to email newsletter',
|
|
745
|
-
});
|
|
746
|
-
if (isDisqualify)
|
|
747
|
-
isValid = false;
|
|
748
|
-
}
|
|
749
|
-
else {
|
|
750
|
-
if (isDisqualify)
|
|
751
|
-
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
// Calculate top-level percentCompleted as average of all condition percentages
|
|
756
|
-
const percentCompleted = conditionData.length > 0
|
|
757
|
-
? conditionData.reduce((sum, c) => sum + c.percentCompleted, 0) / conditionData.length
|
|
758
|
-
: isValid
|
|
759
|
-
? 100
|
|
760
|
-
: 0;
|
|
761
|
-
return {
|
|
762
|
-
/** @deprecated Use isComplete instead */
|
|
763
|
-
isValid,
|
|
764
|
-
isComplete: isValid,
|
|
765
|
-
percentCompleted,
|
|
766
|
-
conditionData: addDetails ? conditionData : undefined,
|
|
767
|
-
};
|
|
768
|
-
};
|
|
769
|
-
exports.meetsBaseConditions = meetsBaseConditions;
|
|
770
|
-
const hasCompletionConditions = (conditions) => {
|
|
771
|
-
if (!conditions)
|
|
772
|
-
return false;
|
|
773
|
-
if (Object.keys(conditions.currencies || {}).length > 0)
|
|
774
|
-
return true;
|
|
775
|
-
if (Object.keys(conditions.levels || {}).length > 0)
|
|
776
|
-
return true;
|
|
777
|
-
if (Object.keys(conditions.stakedTokens || {}).length > 0)
|
|
778
|
-
return true;
|
|
779
|
-
if (Object.keys(conditions.memberships || {}).length > 0)
|
|
780
|
-
return true;
|
|
781
|
-
if (Object.keys(conditions.quests || {}).length > 0)
|
|
782
|
-
return true;
|
|
783
|
-
if (conditions.minTrustScore)
|
|
784
|
-
return true;
|
|
785
|
-
if (conditions.maxTrustScore)
|
|
786
|
-
return true;
|
|
787
|
-
if (conditions.achievements)
|
|
788
|
-
return true;
|
|
789
|
-
if (conditions.minDaysInGame)
|
|
790
|
-
return true;
|
|
791
|
-
if (conditions.dynamic?.conditions?.length)
|
|
792
|
-
return true;
|
|
793
|
-
if (conditions.identifiers?.platforms?.length)
|
|
794
|
-
return true;
|
|
795
|
-
const compCond = conditions;
|
|
796
|
-
if (compCond.context)
|
|
797
|
-
return true;
|
|
798
|
-
if (compCond.buyItem)
|
|
799
|
-
return true;
|
|
800
|
-
if (compCond.spendCurrency)
|
|
801
|
-
return true;
|
|
802
|
-
if (compCond.depositCurrency)
|
|
803
|
-
return true;
|
|
804
|
-
if (compCond.social)
|
|
805
|
-
return true;
|
|
806
|
-
if (compCond.login)
|
|
807
|
-
return true;
|
|
808
|
-
if (compCond.loginStreak)
|
|
809
|
-
return true;
|
|
810
|
-
if (compCond.linkedCompletions)
|
|
811
|
-
return true;
|
|
812
|
-
if (compCond.dynamicTracker?.conditions?.length)
|
|
813
|
-
return true;
|
|
814
|
-
if (conditions.tokenBalances?.length)
|
|
815
|
-
return true;
|
|
816
|
-
if (Object.keys(compCond.contractInteractions || {}).length > 0)
|
|
817
|
-
return true;
|
|
818
|
-
if (conditions.stackedAccount)
|
|
819
|
-
return true;
|
|
820
|
-
if (conditions.userSettings)
|
|
821
|
-
return true;
|
|
822
|
-
return false;
|
|
823
|
-
};
|
|
824
|
-
exports.hasCompletionConditions = hasCompletionConditions;
|
|
825
|
-
const meetsLinkedEntityOffersCondition = ({ linkedEntityOffers, matchingLinks, linkedPOfferMap, }) => {
|
|
826
|
-
if (!linkedPOfferMap)
|
|
827
|
-
return { isValid: false };
|
|
828
|
-
const linkedPlayerOffer_ids = [];
|
|
829
|
-
for (const link of matchingLinks) {
|
|
830
|
-
const key = `${link.playerId}:${linkedEntityOffers.offer_id}`;
|
|
831
|
-
const po = linkedPOfferMap.get(key);
|
|
832
|
-
if (po) {
|
|
833
|
-
linkedPlayerOffer_ids.push(po.instanceId.toString());
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
if (linkedPlayerOffer_ids.length > 0) {
|
|
837
|
-
return { isValid: true, linkedPlayerOffer_ids };
|
|
838
|
-
}
|
|
839
|
-
return { isValid: false };
|
|
840
|
-
};
|
|
841
|
-
exports.meetsLinkedEntityOffersCondition = meetsLinkedEntityOffersCondition;
|
|
842
|
-
const offerMeetsCompletionConditions = (offer, snapshot, additionalData) => {
|
|
843
|
-
return (0, exports.meetsCompletionConditions)({
|
|
844
|
-
completionConditions: offer.completionConditions || {},
|
|
845
|
-
completionTrackers: offer.completionTrackers,
|
|
846
|
-
playerSnap: snapshot,
|
|
847
|
-
playerOffer: offer,
|
|
848
|
-
addDetails: true,
|
|
849
|
-
maxClaimCount: offer.maxClaimCount,
|
|
850
|
-
additionalData,
|
|
851
|
-
});
|
|
852
|
-
};
|
|
853
|
-
exports.offerMeetsCompletionConditions = offerMeetsCompletionConditions;
|
|
854
|
-
const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, additionalData, }) => {
|
|
855
|
-
if (completionConditions) {
|
|
856
|
-
const conditions = completionConditions;
|
|
857
|
-
// For multi-claim offers, scale cumulative requirements by (claimedCount + 1)
|
|
858
|
-
const shouldScale = maxClaimCount === -1 || (maxClaimCount && maxClaimCount > 1);
|
|
859
|
-
const claimMultiplier = shouldScale ? (playerOffer.trackers?.claimedCount || 0) + 1 : 1;
|
|
860
|
-
const conditionData = [];
|
|
861
|
-
let isValid = true;
|
|
862
|
-
let maxTotalClaimsFromScaling = Infinity;
|
|
863
|
-
const updateMax = (limit) => (maxTotalClaimsFromScaling = Math.min(maxTotalClaimsFromScaling, limit));
|
|
864
|
-
if (completionConditions?.context?.id) {
|
|
865
|
-
const hasTrackedContext = completionTrackers?.context && completionConditions.context.id === completionTrackers.context;
|
|
866
|
-
const isDisqualify = !hasTrackedContext;
|
|
867
|
-
if (addDetails) {
|
|
868
|
-
conditionData.push({
|
|
869
|
-
isMet: !isDisqualify,
|
|
870
|
-
kind: 'context',
|
|
871
|
-
trackerAmount: hasTrackedContext ? 1 : 0,
|
|
872
|
-
trackerGoal: 1,
|
|
873
|
-
percentCompleted: !isDisqualify ? 100 : 0, // Binary
|
|
874
|
-
text: completionConditions.context.name,
|
|
875
|
-
});
|
|
876
|
-
if (isDisqualify)
|
|
877
|
-
isValid = false;
|
|
878
|
-
}
|
|
879
|
-
else {
|
|
880
|
-
if (isDisqualify)
|
|
881
|
-
return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
if (conditions?.buyItem) {
|
|
885
|
-
const baseAmount = conditions.buyItem.amount || 1;
|
|
886
|
-
const scaledAmount = baseAmount * claimMultiplier;
|
|
887
|
-
const trackerValue = completionTrackers?.buyItem || 0;
|
|
888
|
-
const isDisqualify = trackerValue < scaledAmount;
|
|
889
|
-
if (shouldScale && baseAmount > 0) {
|
|
890
|
-
updateMax(Math.floor(trackerValue / baseAmount));
|
|
891
|
-
}
|
|
892
|
-
if (addDetails) {
|
|
893
|
-
conditionData.push({
|
|
894
|
-
isMet: !isDisqualify,
|
|
895
|
-
kind: 'buyItem',
|
|
896
|
-
trackerAmount: trackerValue,
|
|
897
|
-
trackerGoal: scaledAmount,
|
|
898
|
-
percentCompleted: calculatePercent(trackerValue, scaledAmount, !isDisqualify),
|
|
899
|
-
text: `Buy ${scaledAmount} ${conditions.buyItem.name}`,
|
|
900
|
-
});
|
|
901
|
-
if (isDisqualify)
|
|
902
|
-
isValid = false;
|
|
903
|
-
}
|
|
904
|
-
else {
|
|
905
|
-
if (isDisqualify)
|
|
906
|
-
return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
if (conditions?.spendCurrency) {
|
|
910
|
-
const baseAmount = conditions.spendCurrency.amount || 1;
|
|
911
|
-
const scaledAmount = baseAmount * claimMultiplier;
|
|
912
|
-
const trackerValue = completionTrackers?.spendCurrency || 0;
|
|
913
|
-
const isDisqualify = trackerValue < scaledAmount;
|
|
914
|
-
if (shouldScale && baseAmount > 0) {
|
|
915
|
-
updateMax(Math.floor(trackerValue / baseAmount));
|
|
916
|
-
}
|
|
917
|
-
if (addDetails) {
|
|
918
|
-
conditionData.push({
|
|
919
|
-
isMet: !isDisqualify,
|
|
920
|
-
kind: 'spendCurrency',
|
|
921
|
-
trackerAmount: trackerValue,
|
|
922
|
-
trackerGoal: scaledAmount,
|
|
923
|
-
percentCompleted: calculatePercent(trackerValue, scaledAmount, !isDisqualify),
|
|
924
|
-
text: `Spend ${scaledAmount} ${conditions.spendCurrency.name}`,
|
|
925
|
-
});
|
|
926
|
-
if (isDisqualify)
|
|
927
|
-
isValid = false;
|
|
928
|
-
}
|
|
929
|
-
else {
|
|
930
|
-
if (isDisqualify)
|
|
931
|
-
return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
if (conditions?.depositCurrency) {
|
|
935
|
-
const baseAmount = conditions.depositCurrency.amount || 1;
|
|
936
|
-
const scaledAmount = baseAmount * claimMultiplier;
|
|
937
|
-
const trackerValue = completionTrackers?.depositCurrency || 0;
|
|
938
|
-
const isDisqualify = trackerValue < scaledAmount;
|
|
939
|
-
if (shouldScale && baseAmount > 0) {
|
|
940
|
-
updateMax(Math.floor(trackerValue / baseAmount));
|
|
941
|
-
}
|
|
942
|
-
if (addDetails) {
|
|
943
|
-
conditionData.push({
|
|
944
|
-
isMet: !isDisqualify,
|
|
945
|
-
kind: 'depositCurrency',
|
|
946
|
-
trackerAmount: trackerValue,
|
|
947
|
-
trackerGoal: scaledAmount,
|
|
948
|
-
percentCompleted: calculatePercent(trackerValue, scaledAmount, !isDisqualify),
|
|
949
|
-
text: `Deposit ${scaledAmount} ${conditions.depositCurrency.name}`,
|
|
950
|
-
});
|
|
951
|
-
if (isDisqualify)
|
|
952
|
-
isValid = false;
|
|
953
|
-
}
|
|
954
|
-
else {
|
|
955
|
-
if (isDisqualify)
|
|
956
|
-
return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
if (conditions?.login) {
|
|
960
|
-
const isMet = new Date(playerSnap.snapshotLastUpdated || 0).getTime() > new Date(playerOffer.createdAt || 0).getTime();
|
|
961
|
-
if (addDetails) {
|
|
962
|
-
conditionData.push({
|
|
963
|
-
isMet,
|
|
964
|
-
kind: 'login',
|
|
965
|
-
trackerAmount: isMet ? 1 : 0,
|
|
966
|
-
trackerGoal: 1,
|
|
967
|
-
percentCompleted: isMet ? 100 : 0, // Binary
|
|
968
|
-
text: `Login to the game`,
|
|
969
|
-
});
|
|
970
|
-
if (!isMet)
|
|
971
|
-
isValid = false;
|
|
972
|
-
}
|
|
973
|
-
else {
|
|
974
|
-
if (!isMet)
|
|
975
|
-
return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
if (conditions?.loginStreak) {
|
|
979
|
-
// player's login streak snapshot right now - their login streak when offer was surfaced = their login streak since the offer was surfaced
|
|
980
|
-
// if their login streak since the offer was surfaced is less than the required login streak, then they are not yet eligible for the offer
|
|
981
|
-
const trackerAmount = (playerSnap.loginStreak || 0) - (completionTrackers?.currentLoginStreak || 0) + 1;
|
|
982
|
-
const trackerGoal = conditions.loginStreak;
|
|
983
|
-
const isDisqualify = trackerAmount < trackerGoal;
|
|
984
|
-
if (addDetails) {
|
|
985
|
-
conditionData.push({
|
|
986
|
-
isMet: !isDisqualify,
|
|
987
|
-
kind: 'loginStreak',
|
|
988
|
-
trackerAmount,
|
|
989
|
-
trackerGoal,
|
|
990
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
991
|
-
text: `Login streak of ${trackerGoal || 0} days`,
|
|
992
|
-
});
|
|
993
|
-
if (isDisqualify)
|
|
994
|
-
isValid = false;
|
|
995
|
-
}
|
|
996
|
-
else {
|
|
997
|
-
if (isDisqualify)
|
|
998
|
-
return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
if (conditions?.social) {
|
|
1002
|
-
const tSocialAccumulate = completionTrackers?.social;
|
|
1003
|
-
const tSocialAttach = completionTrackers?.social;
|
|
1004
|
-
const cSocial = completionConditions.social;
|
|
1005
|
-
const mode = cSocial?.mode || 'attach';
|
|
1006
|
-
const tSocial = mode === 'accumulate' ? tSocialAccumulate : tSocialAttach;
|
|
1007
|
-
const hasContent = Boolean(mode === 'accumulate' ? tSocialAccumulate?.matchCount > 0 : tSocialAttach?.videoId);
|
|
1008
|
-
// Only scale social metrics in accumulate mode (attach mode is single content)
|
|
1009
|
-
const socialMultiplier = mode === 'accumulate' ? claimMultiplier : 1;
|
|
1010
|
-
const minLikes = (cSocial?.minLikes || 0) * socialMultiplier;
|
|
1011
|
-
const minViews = (cSocial?.minViews || 0) * socialMultiplier;
|
|
1012
|
-
const minComments = (cSocial?.minComments || 0) * socialMultiplier;
|
|
1013
|
-
const likes = tSocial?.likes || 0;
|
|
1014
|
-
const views = tSocial?.views || 0;
|
|
1015
|
-
const comments = tSocial?.comments || 0;
|
|
1016
|
-
let isDisqualify = !hasContent;
|
|
1017
|
-
if (likes < minLikes || views < minViews || comments < minComments) {
|
|
1018
|
-
isDisqualify = true;
|
|
1019
|
-
}
|
|
1020
|
-
if (shouldScale && mode === 'accumulate' && hasContent) {
|
|
1021
|
-
const baseLikes = cSocial?.minLikes || 0;
|
|
1022
|
-
const baseViews = cSocial?.minViews || 0;
|
|
1023
|
-
const baseComments = cSocial?.minComments || 0;
|
|
1024
|
-
if (baseLikes > 0)
|
|
1025
|
-
updateMax(Math.floor(likes / baseLikes));
|
|
1026
|
-
if (baseViews > 0)
|
|
1027
|
-
updateMax(Math.floor(views / baseViews));
|
|
1028
|
-
if (baseComments > 0)
|
|
1029
|
-
updateMax(Math.floor(comments / baseComments));
|
|
1030
|
-
}
|
|
1031
|
-
// Calculate social percentage - average of all applicable metrics
|
|
1032
|
-
const socialPercentages = [];
|
|
1033
|
-
if (minLikes > 0)
|
|
1034
|
-
socialPercentages.push(calculatePercent(likes, minLikes, likes >= minLikes));
|
|
1035
|
-
if (minViews > 0)
|
|
1036
|
-
socialPercentages.push(calculatePercent(views, minViews, views >= minViews));
|
|
1037
|
-
if (minComments > 0)
|
|
1038
|
-
socialPercentages.push(calculatePercent(comments, minComments, comments >= minComments));
|
|
1039
|
-
if (!hasContent)
|
|
1040
|
-
socialPercentages.push(0);
|
|
1041
|
-
const socialPercent = socialPercentages.length > 0
|
|
1042
|
-
? socialPercentages.reduce((a, b) => a + b, 0) / socialPercentages.length
|
|
1043
|
-
: hasContent
|
|
1044
|
-
? 100
|
|
1045
|
-
: 0;
|
|
1046
|
-
if (addDetails) {
|
|
1047
|
-
const platformMap = {
|
|
1048
|
-
tiktok: 'TikTok',
|
|
1049
|
-
instagram: 'Instagram',
|
|
1050
|
-
youtube: 'YouTube',
|
|
1051
|
-
};
|
|
1052
|
-
const platformText = conditions.social.platforms.map((platform) => platformMap[platform]).join(' | ');
|
|
1053
|
-
const requiredWords = cSocial?.requiredWords ?? [];
|
|
1054
|
-
if (mode === 'accumulate') {
|
|
1055
|
-
const matchCount = tSocialAccumulate?.matchCount || 0;
|
|
1056
|
-
conditionData.push({
|
|
1057
|
-
isMet: hasContent && !isDisqualify,
|
|
1058
|
-
kind: 'social',
|
|
1059
|
-
trackerAmount: matchCount,
|
|
1060
|
-
trackerGoal: 1,
|
|
1061
|
-
percentCompleted: socialPercent,
|
|
1062
|
-
text: hasContent
|
|
1063
|
-
? `Found ${matchCount} matching ${platformText} post${matchCount !== 1 ? 's' : ''}`
|
|
1064
|
-
: requiredWords.length > 0
|
|
1065
|
-
? `Post ${platformText} content with ${requiredWords.map((w) => `"${w}"`).join(', ')}`
|
|
1066
|
-
: `Post ${platformText} content`,
|
|
1067
|
-
});
|
|
1068
|
-
}
|
|
1069
|
-
else {
|
|
1070
|
-
const title = tSocialAttach?.title;
|
|
1071
|
-
conditionData.push({
|
|
1072
|
-
isMet: hasContent && !isDisqualify,
|
|
1073
|
-
kind: 'social',
|
|
1074
|
-
trackerAmount: hasContent ? 1 : 0,
|
|
1075
|
-
trackerGoal: 1,
|
|
1076
|
-
percentCompleted: socialPercent,
|
|
1077
|
-
text: !hasContent
|
|
1078
|
-
? requiredWords.length > 0
|
|
1079
|
-
? `Attach a ${platformText} post with ${requiredWords.map((w) => `"${w}"`).join(', ')} in the title`
|
|
1080
|
-
: `Attach a ${platformText} post`
|
|
1081
|
-
: `Attached: ${title}`,
|
|
1082
|
-
});
|
|
1083
|
-
}
|
|
1084
|
-
if (minLikes > 0) {
|
|
1085
|
-
conditionData.push({
|
|
1086
|
-
isMet: hasContent && likes >= minLikes,
|
|
1087
|
-
kind: 'social',
|
|
1088
|
-
trackerAmount: likes,
|
|
1089
|
-
trackerGoal: minLikes,
|
|
1090
|
-
percentCompleted: calculatePercent(likes, minLikes, hasContent && likes >= minLikes),
|
|
1091
|
-
text: mode === 'accumulate' ? `Combined ${minLikes} Likes` : `Reach ${minLikes} Likes`,
|
|
1092
|
-
});
|
|
1093
|
-
}
|
|
1094
|
-
if (minViews > 0) {
|
|
1095
|
-
conditionData.push({
|
|
1096
|
-
isMet: hasContent && views >= minViews,
|
|
1097
|
-
kind: 'social',
|
|
1098
|
-
trackerAmount: views,
|
|
1099
|
-
trackerGoal: minViews,
|
|
1100
|
-
percentCompleted: calculatePercent(views, minViews, hasContent && views >= minViews),
|
|
1101
|
-
text: mode === 'accumulate' ? `Combined ${minViews} Views` : `Reach ${minViews} Views`,
|
|
1102
|
-
});
|
|
1103
|
-
}
|
|
1104
|
-
if (minComments > 0) {
|
|
1105
|
-
conditionData.push({
|
|
1106
|
-
isMet: hasContent && comments >= minComments,
|
|
1107
|
-
kind: 'social',
|
|
1108
|
-
trackerAmount: comments,
|
|
1109
|
-
trackerGoal: minComments,
|
|
1110
|
-
percentCompleted: calculatePercent(comments, minComments, hasContent && comments >= minComments),
|
|
1111
|
-
text: mode === 'accumulate' ? `Combined ${minComments} Comments` : `Reach ${minComments} Comments`,
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
if (isDisqualify)
|
|
1115
|
-
isValid = false;
|
|
1116
|
-
}
|
|
1117
|
-
else {
|
|
1118
|
-
if (isDisqualify)
|
|
1119
|
-
return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
// Linked completions - wait for N linked entities to complete
|
|
1123
|
-
if (conditions?.linkedCompletions?.min) {
|
|
1124
|
-
const baseMin = conditions.linkedCompletions.min;
|
|
1125
|
-
const trackerAmount = completionTrackers?.linkedCompletions || 0;
|
|
1126
|
-
const trackerGoal = baseMin * claimMultiplier;
|
|
1127
|
-
const isDisqualify = trackerAmount < trackerGoal;
|
|
1128
|
-
if (shouldScale && baseMin > 0) {
|
|
1129
|
-
updateMax(Math.floor(trackerAmount / baseMin));
|
|
1130
|
-
}
|
|
1131
|
-
if (addDetails) {
|
|
1132
|
-
conditionData.push({
|
|
1133
|
-
isMet: !isDisqualify,
|
|
1134
|
-
kind: 'linkedCompletions',
|
|
1135
|
-
trackerAmount,
|
|
1136
|
-
trackerGoal,
|
|
1137
|
-
percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
|
|
1138
|
-
text: conditions.linkedCompletions.template
|
|
1139
|
-
? (0, template_1.renderTemplate)(conditions.linkedCompletions.template, {
|
|
1140
|
-
current: trackerAmount,
|
|
1141
|
-
required: trackerGoal,
|
|
1142
|
-
})
|
|
1143
|
-
: `Wait for ${trackerGoal} linked ${trackerGoal === 1 ? 'entity' : 'entities'} to complete`,
|
|
1144
|
-
});
|
|
1145
|
-
if (isDisqualify)
|
|
1146
|
-
isValid = false;
|
|
1147
|
-
}
|
|
1148
|
-
else {
|
|
1149
|
-
if (isDisqualify)
|
|
1150
|
-
return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
if (conditions?.dynamicTracker?.conditions?.length) {
|
|
1154
|
-
const resolvedConditions = (0, template_1.replaceDynamicConditionKeys)(conditions.dynamicTracker.conditions, playerOffer?.trackers || {});
|
|
1155
|
-
// Convert { key: { value: X } } to { key: X } format
|
|
1156
|
-
const primitiveTrackers = (0, dynamic_1.dynamicTrackerToPrimitive)(completionTrackers?.dynamicTracker || {});
|
|
1157
|
-
const dynamicResult = meetsDynamicConditions(primitiveTrackers, {
|
|
1158
|
-
...conditions.dynamicTracker,
|
|
1159
|
-
conditions: resolvedConditions,
|
|
1160
|
-
}, claimMultiplier);
|
|
1161
|
-
if (shouldScale) {
|
|
1162
|
-
const dynamicMax = getMaxClaimsForDynamicGroup(primitiveTrackers, {
|
|
1163
|
-
...conditions.dynamicTracker,
|
|
1164
|
-
conditions: resolvedConditions,
|
|
1165
|
-
}, playerOffer?.trackers?.claimedCount || 0);
|
|
1166
|
-
updateMax(dynamicMax);
|
|
1167
|
-
}
|
|
1168
|
-
if (addDetails) {
|
|
1169
|
-
// Extract tracker amount and goal from the first condition for display
|
|
1170
|
-
let trackerAmount;
|
|
1171
|
-
let trackerGoal;
|
|
1172
|
-
if (resolvedConditions.length > 0) {
|
|
1173
|
-
const firstCond = resolvedConditions[0];
|
|
1174
|
-
if (typeof firstCond.compareTo === 'number') {
|
|
1175
|
-
trackerGoal = firstCond.compareTo;
|
|
1176
|
-
}
|
|
1177
|
-
const val = primitiveTrackers[firstCond.key];
|
|
1178
|
-
trackerAmount = typeof val === 'number' ? val : 0;
|
|
1179
|
-
}
|
|
1180
|
-
// Calculate percentage using the dynamic group helper for AND/OR logic
|
|
1181
|
-
const percentCompleted = calculateDynamicGroupPercent(primitiveTrackers, {
|
|
1182
|
-
...conditions.dynamicTracker,
|
|
1183
|
-
conditions: resolvedConditions,
|
|
1184
|
-
});
|
|
1185
|
-
conditionData.push({
|
|
1186
|
-
isMet: dynamicResult,
|
|
1187
|
-
kind: 'dynamicTracker',
|
|
1188
|
-
trackerAmount,
|
|
1189
|
-
trackerGoal,
|
|
1190
|
-
percentCompleted,
|
|
1191
|
-
text: (0, template_1.renderTemplate)(
|
|
1192
|
-
// First resolve {{}} placeholders (e.g., {{surfacerPlayerId}}) using offer trackers
|
|
1193
|
-
(0, template_1.replaceDynamicConditionKey)(conditions.dynamicTracker.template || '', playerOffer?.trackers || {}), primitiveTrackers) || 'Dynamic conditions',
|
|
1194
|
-
});
|
|
1195
|
-
if (!dynamicResult)
|
|
1196
|
-
isValid = false;
|
|
1197
|
-
}
|
|
1198
|
-
else {
|
|
1199
|
-
if (!dynamicResult)
|
|
1200
|
-
return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
// Evaluate contractInteractions completion trackers
|
|
1204
|
-
if (conditions?.contractInteractions) {
|
|
1205
|
-
const interactions = conditions.contractInteractions;
|
|
1206
|
-
const entries = Array.isArray(interactions)
|
|
1207
|
-
? interactions.map((c) => [c.id || '', c])
|
|
1208
|
-
: Object.entries(interactions);
|
|
1209
|
-
for (const [conditionId, condition] of entries) {
|
|
1210
|
-
// Tracker now stores { amount: number, count: number }
|
|
1211
|
-
// Default to { amount: 0, count: 0 } if missing
|
|
1212
|
-
const rawTracker = completionTrackers?.contractInteractions?.[conditionId];
|
|
1213
|
-
const trackerData = rawTracker || { amount: 0, count: 0 };
|
|
1214
|
-
const trackerAmount = trackerData.amount || 0;
|
|
1215
|
-
const trackerCount = trackerData.count || 0;
|
|
1216
|
-
// Goals
|
|
1217
|
-
const minAmount = condition.minAmount ? condition.minAmount * claimMultiplier : 0;
|
|
1218
|
-
const maxAmount = condition.maxAmount ? condition.maxAmount * claimMultiplier : undefined;
|
|
1219
|
-
const minCount = condition.minCount ? condition.minCount * claimMultiplier : 0;
|
|
1220
|
-
const maxCount = condition.maxCount ? condition.maxCount * claimMultiplier : undefined;
|
|
1221
|
-
let isDisqualify = false;
|
|
1222
|
-
// Check Amounts
|
|
1223
|
-
if (shouldScale) {
|
|
1224
|
-
// If we have scaling (claimMultiplier), we handle max claims calculation differently
|
|
1225
|
-
// For now, assume standard validation
|
|
1226
|
-
}
|
|
1227
|
-
if (minAmount > 0 && trackerAmount < minAmount)
|
|
1228
|
-
isDisqualify = true;
|
|
1229
|
-
if (maxAmount !== undefined && trackerAmount > maxAmount)
|
|
1230
|
-
isDisqualify = true;
|
|
1231
|
-
// Check Counts
|
|
1232
|
-
if (minCount > 0 && trackerCount < minCount)
|
|
1233
|
-
isDisqualify = true;
|
|
1234
|
-
if (maxCount !== undefined && trackerCount > maxCount)
|
|
1235
|
-
isDisqualify = true;
|
|
1236
|
-
if (shouldScale) {
|
|
1237
|
-
// Update max claims based on whatever allows the FEWEST claims
|
|
1238
|
-
// e.g. if I have enough Amount for 3 claims but enough Count for 1 claim, I can only make 1 claim.
|
|
1239
|
-
const possibleClaimsByAmount = condition.minAmount
|
|
1240
|
-
? Math.floor(trackerAmount / condition.minAmount)
|
|
1241
|
-
: Number.MAX_SAFE_INTEGER;
|
|
1242
|
-
const possibleClaimsByCount = condition.minCount
|
|
1243
|
-
? Math.floor(trackerCount / condition.minCount)
|
|
1244
|
-
: Number.MAX_SAFE_INTEGER;
|
|
1245
|
-
const possibleClaims = Math.min(possibleClaimsByAmount, possibleClaimsByCount);
|
|
1246
|
-
// Only update if we have at least ANY requirement (Amount or Count)
|
|
1247
|
-
// If both minAmount and minCount are 0/undefined, this logic might be weird, but usually one is set.
|
|
1248
|
-
if (condition.minAmount || condition.minCount) {
|
|
1249
|
-
updateMax(possibleClaims);
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
if (addDetails) {
|
|
1253
|
-
// Determine primary metric for progress display
|
|
1254
|
-
// If minAmount is set, use amount. Else use count.
|
|
1255
|
-
let current = trackerAmount;
|
|
1256
|
-
let goal = minAmount;
|
|
1257
|
-
let percent = 0;
|
|
1258
|
-
// If both are set, display percent based on the average of the two
|
|
1259
|
-
const hasMin = minAmount > 0 || minCount > 0;
|
|
1260
|
-
const hasMax = (maxAmount !== undefined && maxAmount > 0) || (maxCount !== undefined && maxCount > 0);
|
|
1261
|
-
if (!hasMin && hasMax) {
|
|
1262
|
-
// If only max is set: > 0 progress = 100%, else 0%
|
|
1263
|
-
const maxAmountSet = maxAmount !== undefined && maxAmount > 0;
|
|
1264
|
-
const maxCountSet = maxCount !== undefined && maxCount > 0;
|
|
1265
|
-
let pcts = [];
|
|
1266
|
-
if (maxAmountSet) {
|
|
1267
|
-
pcts.push(trackerAmount > 0 ? 100 : 0);
|
|
1268
|
-
}
|
|
1269
|
-
if (maxCountSet) {
|
|
1270
|
-
pcts.push(trackerCount > 0 ? 100 : 0);
|
|
1271
|
-
}
|
|
1272
|
-
if (pcts.length > 0) {
|
|
1273
|
-
percent = pcts.reduce((a, b) => a + b, 0) / pcts.length;
|
|
1274
|
-
}
|
|
1275
|
-
else {
|
|
1276
|
-
percent = 0;
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
else if (minAmount > 0 && minCount > 0) {
|
|
1280
|
-
const pctAmount = calculatePercent(trackerAmount, minAmount, true); // clamp to 100
|
|
1281
|
-
const pctCount = calculatePercent(trackerCount, minCount, true);
|
|
1282
|
-
percent = (pctAmount + pctCount) / 2;
|
|
1283
|
-
}
|
|
1284
|
-
else if (minCount > 0) {
|
|
1285
|
-
current = trackerCount;
|
|
1286
|
-
goal = minCount;
|
|
1287
|
-
percent = calculatePercent(current, goal, !isDisqualify);
|
|
1288
|
-
}
|
|
1289
|
-
else {
|
|
1290
|
-
percent = calculatePercent(trackerAmount, minAmount || 0, !isDisqualify);
|
|
1291
|
-
}
|
|
1292
|
-
const displayText = (0, template_1.renderTemplate)(condition.template, {
|
|
1293
|
-
currentAmount: trackerAmount || 0,
|
|
1294
|
-
currentCount: trackerCount || 0,
|
|
1295
|
-
minAmount: minAmount || 0,
|
|
1296
|
-
maxAmount: maxAmount || 0,
|
|
1297
|
-
minCount: minCount || 0,
|
|
1298
|
-
maxCount: maxCount || 0,
|
|
1299
|
-
});
|
|
1300
|
-
conditionData.push({
|
|
1301
|
-
isMet: !isDisqualify,
|
|
1302
|
-
kind: 'contractInteractions',
|
|
1303
|
-
trackerAmount,
|
|
1304
|
-
percentCompleted: percent,
|
|
1305
|
-
text: displayText,
|
|
1306
|
-
});
|
|
1307
|
-
if (isDisqualify)
|
|
1308
|
-
isValid = false;
|
|
1309
|
-
}
|
|
1310
|
-
else {
|
|
1311
|
-
if (isDisqualify)
|
|
1312
|
-
return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
const r = (0, exports.meetsBaseConditions)({
|
|
1317
|
-
conditions,
|
|
1318
|
-
playerSnap,
|
|
1319
|
-
addDetails: true,
|
|
1320
|
-
playerOffer,
|
|
1321
|
-
additionalData,
|
|
1322
|
-
});
|
|
1323
|
-
isValid = isValid && r.isValid;
|
|
1324
|
-
conditionData.push(...(r.conditionData || []));
|
|
1325
|
-
if (maxClaimCount && maxClaimCount > 0) {
|
|
1326
|
-
updateMax(maxClaimCount);
|
|
1327
|
-
}
|
|
1328
|
-
const claimedCount = playerOffer?.trackers?.claimedCount || 0;
|
|
1329
|
-
const availableClaimsNow = !isValid
|
|
1330
|
-
? 0
|
|
1331
|
-
: maxTotalClaimsFromScaling === Infinity
|
|
1332
|
-
? -1
|
|
1333
|
-
: Math.max(0, maxTotalClaimsFromScaling - claimedCount);
|
|
1334
|
-
// Calculate top-level percentCompleted as average of all condition percentages
|
|
1335
|
-
const percentCompleted = conditionData.length > 0
|
|
1336
|
-
? conditionData.reduce((sum, c) => sum + c.percentCompleted, 0) / conditionData.length
|
|
1337
|
-
: isValid
|
|
1338
|
-
? 100
|
|
1339
|
-
: 0;
|
|
1340
|
-
return {
|
|
1341
|
-
/** @deprecated Use isComplete instead */
|
|
1342
|
-
isValid,
|
|
1343
|
-
isComplete: isValid,
|
|
1344
|
-
percentCompleted,
|
|
1345
|
-
conditionData,
|
|
1346
|
-
availableClaimsNow,
|
|
1347
|
-
};
|
|
1348
|
-
}
|
|
1349
|
-
return { isValid: true, isComplete: true, percentCompleted: 100, conditionData: [], availableClaimsNow: -1 };
|
|
1350
|
-
};
|
|
1351
|
-
exports.meetsCompletionConditions = meetsCompletionConditions;
|
|
1352
|
-
/**
|
|
1353
|
-
* Checks if completion conditions were met before a specific expiry time.
|
|
1354
|
-
* Returns true if all relevant condition fields were updated before expiryTime.
|
|
1355
|
-
*
|
|
1356
|
-
* @param completionConditions - The completion conditions to check
|
|
1357
|
-
* @param completionTrackers - The completion trackers (for buyItem, spendCurrency, etc.)
|
|
1358
|
-
* @param playerSnap - The player snapshot with field timestamps
|
|
1359
|
-
* @returns true if all conditions were met before expiry, false otherwise
|
|
1360
|
-
*/
|
|
1361
|
-
const meetsCompletionConditionsBeforeExpiry = ({ completionConditions, completionTrackers, playerSnap, playerOffer, maxClaimCount, }) => {
|
|
1362
|
-
if (!completionConditions)
|
|
1363
|
-
return false;
|
|
1364
|
-
// Check if there are actually any conditions to evaluate
|
|
1365
|
-
if (!(0, exports.hasCompletionConditions)(completionConditions))
|
|
1366
|
-
return false;
|
|
1367
|
-
// First check if conditions are actually met
|
|
1368
|
-
const conditionsMet = (0, exports.meetsCompletionConditions)({
|
|
1369
|
-
completionConditions,
|
|
1370
|
-
completionTrackers,
|
|
1371
|
-
playerOffer,
|
|
1372
|
-
playerSnap,
|
|
1373
|
-
maxClaimCount,
|
|
1374
|
-
});
|
|
1375
|
-
if (!conditionsMet.isValid)
|
|
1376
|
-
return false;
|
|
1377
|
-
if (!playerOffer.expiresAt)
|
|
1378
|
-
return true;
|
|
1379
|
-
const expiryTime = new Date(playerOffer.expiresAt).getTime();
|
|
1380
|
-
const lastSnapshotUpdate = new Date(playerSnap.snapshotLastUpdated ?? new Date()).getTime();
|
|
1381
|
-
/**
|
|
1382
|
-
* Checks if a field was updated after the expiry time.
|
|
1383
|
-
* Returns true if updated AFTER or AT expiry (violates grace period).
|
|
1384
|
-
* Returns false if updated BEFORE expiry (allows grace period).
|
|
1385
|
-
*/
|
|
1386
|
-
function wasUpdatedAfterExpiry(data) {
|
|
1387
|
-
let lastUpdated;
|
|
1388
|
-
if (typeof data === 'object' && data !== null && !(data instanceof Date)) {
|
|
1389
|
-
// Object with optional lastUpdated field
|
|
1390
|
-
lastUpdated = data.lastUpdated ? new Date(data.lastUpdated).getTime() : lastSnapshotUpdate;
|
|
1391
|
-
}
|
|
1392
|
-
else if (data instanceof Date) {
|
|
1393
|
-
lastUpdated = data.getTime();
|
|
1394
|
-
}
|
|
1395
|
-
else if (typeof data === 'string' || typeof data === 'number') {
|
|
1396
|
-
lastUpdated = new Date(data).getTime();
|
|
1397
|
-
}
|
|
1398
|
-
else {
|
|
1399
|
-
// No data provided, use snapshot timestamp
|
|
1400
|
-
lastUpdated = lastSnapshotUpdate;
|
|
1401
|
-
}
|
|
1402
|
-
return lastUpdated >= expiryTime;
|
|
1403
|
-
}
|
|
1404
|
-
if (completionConditions.currencies) {
|
|
1405
|
-
for (const currencyId in completionConditions.currencies) {
|
|
1406
|
-
const currency = playerSnap.currencies?.[currencyId];
|
|
1407
|
-
if (!currency)
|
|
1408
|
-
continue;
|
|
1409
|
-
if (wasUpdatedAfterExpiry(currency))
|
|
1410
|
-
return false;
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
if (completionConditions.levels) {
|
|
1414
|
-
for (const skillId in completionConditions.levels) {
|
|
1415
|
-
const level = playerSnap.levels?.[skillId];
|
|
1416
|
-
if (!level)
|
|
1417
|
-
continue;
|
|
1418
|
-
if (wasUpdatedAfterExpiry(level))
|
|
1419
|
-
return false;
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
if (completionConditions.quests) {
|
|
1423
|
-
for (const questId in completionConditions.quests) {
|
|
1424
|
-
const quest = playerSnap.quests?.[questId];
|
|
1425
|
-
if (!quest)
|
|
1426
|
-
continue;
|
|
1427
|
-
if (wasUpdatedAfterExpiry(quest))
|
|
1428
|
-
return false;
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
if (completionConditions.memberships) {
|
|
1432
|
-
for (const membershipId in completionConditions.memberships) {
|
|
1433
|
-
const membership = playerSnap.memberships?.[membershipId];
|
|
1434
|
-
if (!membership)
|
|
1435
|
-
continue;
|
|
1436
|
-
if (wasUpdatedAfterExpiry(membership))
|
|
1437
|
-
return false;
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
if (completionConditions.achievements) {
|
|
1441
|
-
for (const achievementId in completionConditions.achievements) {
|
|
1442
|
-
const achievement = playerSnap.achievements?.[achievementId];
|
|
1443
|
-
if (!achievement)
|
|
1444
|
-
continue;
|
|
1445
|
-
if (wasUpdatedAfterExpiry(achievement))
|
|
1446
|
-
return false;
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
if (completionConditions.stakedTokens) {
|
|
1450
|
-
for (const tokenId in completionConditions.stakedTokens) {
|
|
1451
|
-
const stakedToken = playerSnap.stakedTokens?.[tokenId];
|
|
1452
|
-
if (!stakedToken)
|
|
1453
|
-
continue;
|
|
1454
|
-
const lastStakeTime = new Date(stakedToken.lastStake ?? 0).getTime();
|
|
1455
|
-
const lastUnstakeTime = new Date(stakedToken.lastUnstake ?? 0).getTime();
|
|
1456
|
-
const lastUpdated = Math.max(lastStakeTime, lastUnstakeTime);
|
|
1457
|
-
if (lastUpdated >= expiryTime)
|
|
1458
|
-
return false;
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
if (completionConditions.minTrustScore !== undefined || completionConditions.maxTrustScore !== undefined) {
|
|
1462
|
-
if (wasUpdatedAfterExpiry(playerSnap.trustLastUpdated))
|
|
1463
|
-
return false;
|
|
1464
|
-
}
|
|
1465
|
-
if (completionConditions.minDaysInGame !== undefined) {
|
|
1466
|
-
if (wasUpdatedAfterExpiry(playerSnap.daysInGameLastUpdated))
|
|
1467
|
-
return false;
|
|
1468
|
-
}
|
|
1469
|
-
if (completionConditions.login || completionConditions.loginStreak) {
|
|
1470
|
-
if (wasUpdatedAfterExpiry())
|
|
1471
|
-
return false;
|
|
1472
|
-
}
|
|
1473
|
-
if (completionConditions.dynamic?.conditions?.length) {
|
|
1474
|
-
if (wasUpdatedAfterExpiry())
|
|
1475
|
-
return false;
|
|
1476
|
-
}
|
|
1477
|
-
if (completionConditions.identifiers?.platforms?.length) {
|
|
1478
|
-
if (wasUpdatedAfterExpiry())
|
|
1479
|
-
return false;
|
|
1480
|
-
}
|
|
1481
|
-
if (completionConditions.social) {
|
|
1482
|
-
// Check if social content was attached/validated after expiry
|
|
1483
|
-
if (completionTrackers?.social?.lastChecked) {
|
|
1484
|
-
if (wasUpdatedAfterExpiry(completionTrackers.social.lastChecked))
|
|
1485
|
-
return false;
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
// Tracker-based conditions: use completionTrackers.lastUpdated if available.
|
|
1489
|
-
// Legacy offers without lastUpdated skip these checks (preserving current grace-period behavior).
|
|
1490
|
-
if (completionTrackers?.lastUpdated != null) {
|
|
1491
|
-
const trackerUpdatedAfterExpiry = wasUpdatedAfterExpiry(completionTrackers.lastUpdated);
|
|
1492
|
-
if (trackerUpdatedAfterExpiry && (completionConditions.buyItem ||
|
|
1493
|
-
completionConditions.spendCurrency ||
|
|
1494
|
-
completionConditions.depositCurrency ||
|
|
1495
|
-
completionConditions.context ||
|
|
1496
|
-
completionConditions.linkedCompletions ||
|
|
1497
|
-
completionConditions.contractInteractions))
|
|
1498
|
-
return false;
|
|
1499
|
-
if (completionConditions.dynamicTracker?.conditions?.length) {
|
|
1500
|
-
for (const cond of completionConditions.dynamicTracker.conditions) {
|
|
1501
|
-
const perKeyTs = completionTrackers.dynamicTracker?.[cond.key]?.lastUpdated;
|
|
1502
|
-
if (wasUpdatedAfterExpiry(perKeyTs ?? completionTrackers.lastUpdated))
|
|
1503
|
-
return false;
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
// All conditions were met before expiry
|
|
1508
|
-
return true;
|
|
1509
|
-
};
|
|
1510
|
-
exports.meetsCompletionConditionsBeforeExpiry = meetsCompletionConditionsBeforeExpiry;
|
|
1511
|
-
/**
|
|
1512
|
-
* Checks if a dynamic object meets a set of dynamic field conditions.
|
|
1513
|
-
* @param dynamicObj - The object with any key and string or number value.
|
|
1514
|
-
* @param conditions - Array of conditions to check.
|
|
1515
|
-
* @param claimMultiplier - Multiplier to scale conditions (used for numeric comparisons).
|
|
1516
|
-
* @returns true if all conditions are met, false otherwise.
|
|
1517
|
-
*/
|
|
1518
|
-
/**
|
|
1519
|
-
* Evaluates a single dynamic condition against the dynamic object.
|
|
1520
|
-
*/
|
|
1521
|
-
function evaluateDynamicCondition(dynamicObj, cond, claimMultiplier = 1) {
|
|
1522
|
-
if (!dynamicObj)
|
|
1523
|
-
return false;
|
|
1524
|
-
const val = dynamicObj[cond.key];
|
|
1525
|
-
if (cond.operator === 'is_truthy')
|
|
1526
|
-
return !!val;
|
|
1527
|
-
if (cond.operator === 'is_falsy')
|
|
1528
|
-
return !val;
|
|
1529
|
-
if (cond.operator === 'is_defined')
|
|
1530
|
-
return val != null;
|
|
1531
|
-
if (val == undefined)
|
|
1532
|
-
return false;
|
|
1533
|
-
const isNumber = typeof val === 'number';
|
|
1534
|
-
const isBoolean = typeof val === 'boolean';
|
|
1535
|
-
if (isBoolean) {
|
|
1536
|
-
switch (cond.operator) {
|
|
1537
|
-
case '==':
|
|
1538
|
-
return val === Boolean(cond.compareTo);
|
|
1539
|
-
case '!=':
|
|
1540
|
-
return val !== Boolean(cond.compareTo);
|
|
1541
|
-
default:
|
|
1542
|
-
return false;
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
const compareTo = isNumber ? Number(cond.compareTo) : String(cond.compareTo);
|
|
1546
|
-
if (isNumber && typeof compareTo === 'number') {
|
|
1547
|
-
const skipMultiplier = cond.operator === '==' || cond.operator === '!=';
|
|
1548
|
-
const scaledCompareTo = skipMultiplier ? compareTo : compareTo * claimMultiplier;
|
|
1549
|
-
switch (cond.operator) {
|
|
1550
|
-
case '==':
|
|
1551
|
-
return val === scaledCompareTo;
|
|
1552
|
-
case '!=':
|
|
1553
|
-
return val !== scaledCompareTo;
|
|
1554
|
-
case '>':
|
|
1555
|
-
return val > scaledCompareTo;
|
|
1556
|
-
case '>=':
|
|
1557
|
-
return val >= scaledCompareTo;
|
|
1558
|
-
case '<':
|
|
1559
|
-
return val < scaledCompareTo;
|
|
1560
|
-
case '<=':
|
|
1561
|
-
return val <= scaledCompareTo;
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
else if (!isNumber && typeof compareTo === 'string') {
|
|
1565
|
-
switch (cond.operator) {
|
|
1566
|
-
case '==':
|
|
1567
|
-
return val === compareTo;
|
|
1568
|
-
case '!=':
|
|
1569
|
-
return val !== compareTo;
|
|
1570
|
-
case 'has':
|
|
1571
|
-
return val.includes(compareTo);
|
|
1572
|
-
case 'not_has':
|
|
1573
|
-
return !val.includes(compareTo);
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
return false;
|
|
1577
|
-
}
|
|
1578
|
-
/**
|
|
1579
|
-
* Calculates the maximum number of claims supported by a single dynamic condition.
|
|
1580
|
-
*/
|
|
1581
|
-
function getMaxClaimsForDynamicCondition(dynamicObj, cond) {
|
|
1582
|
-
if (!dynamicObj)
|
|
1583
|
-
return 0;
|
|
1584
|
-
if (cond.operator === 'is_truthy')
|
|
1585
|
-
return dynamicObj[cond.key] ? Infinity : 0;
|
|
1586
|
-
if (cond.operator === 'is_falsy')
|
|
1587
|
-
return !dynamicObj[cond.key] ? Infinity : 0;
|
|
1588
|
-
if (cond.operator === 'is_defined')
|
|
1589
|
-
return dynamicObj[cond.key] != null ? Infinity : 0;
|
|
1590
|
-
const val = dynamicObj[cond.key];
|
|
1591
|
-
if (val === undefined)
|
|
1592
|
-
return 0;
|
|
1593
|
-
if (typeof val === 'number') {
|
|
1594
|
-
const base = Number(cond.compareTo);
|
|
1595
|
-
if (isNaN(base)) {
|
|
1596
|
-
return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
|
|
1597
|
-
}
|
|
1598
|
-
switch (cond.operator) {
|
|
1599
|
-
case '>=':
|
|
1600
|
-
if (base === 0)
|
|
1601
|
-
return val >= 0 ? Infinity : 0;
|
|
1602
|
-
if (base < 0)
|
|
1603
|
-
return val >= base ? Infinity : 0;
|
|
1604
|
-
return Math.max(0, Math.floor(val / base));
|
|
1605
|
-
case '>':
|
|
1606
|
-
if (base === 0)
|
|
1607
|
-
return val > 0 ? Infinity : 0;
|
|
1608
|
-
if (base < 0)
|
|
1609
|
-
return val > base ? Infinity : 0;
|
|
1610
|
-
if (val <= 0)
|
|
1611
|
-
return 0;
|
|
1612
|
-
return Math.max(0, Math.ceil(val / base) - 1);
|
|
1613
|
-
case '==':
|
|
1614
|
-
return val === base ? Infinity : 0;
|
|
1615
|
-
case '!=':
|
|
1616
|
-
return val !== base ? Infinity : 0;
|
|
1617
|
-
case '<=':
|
|
1618
|
-
if (base === 0)
|
|
1619
|
-
return val <= 0 ? Infinity : 0;
|
|
1620
|
-
if (base > 0)
|
|
1621
|
-
return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
|
|
1622
|
-
if (val >= 0)
|
|
1623
|
-
return 0;
|
|
1624
|
-
return Math.max(0, Math.floor(val / base));
|
|
1625
|
-
case '<':
|
|
1626
|
-
if (base === 0)
|
|
1627
|
-
return val < 0 ? Infinity : 0;
|
|
1628
|
-
if (base > 0)
|
|
1629
|
-
return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
|
|
1630
|
-
if (val >= 0)
|
|
1631
|
-
return 0;
|
|
1632
|
-
return Math.max(0, Math.ceil(val / base) - 1);
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
// we don't scale the rest, they are always true or always false
|
|
1636
|
-
return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
|
|
1637
|
-
}
|
|
1638
|
-
/**
|
|
1639
|
-
* Calculates the maximum number of claims supported by a group of dynamic conditions.
|
|
1640
|
-
*/
|
|
1641
|
-
function getMaxClaimsForDynamicGroup(dynamicObj, dynamicGroup, currentClaimCount = 0) {
|
|
1642
|
-
const { conditions, links } = dynamicGroup;
|
|
1643
|
-
if (!conditions || conditions.length === 0)
|
|
1644
|
-
return Infinity;
|
|
1645
|
-
// AND only
|
|
1646
|
-
if (!links || links.length === 0 || links.every((l) => l === 'AND')) {
|
|
1647
|
-
let minClaims = Infinity;
|
|
1648
|
-
for (const cond of conditions) {
|
|
1649
|
-
const max = getMaxClaimsForDynamicCondition(dynamicObj, cond);
|
|
1650
|
-
if (max === 0)
|
|
1651
|
-
return 0;
|
|
1652
|
-
minClaims = Math.min(minClaims, max);
|
|
1653
|
-
}
|
|
1654
|
-
return minClaims;
|
|
1655
|
-
}
|
|
1656
|
-
// OR only
|
|
1657
|
-
if (links.every((l) => l === 'OR')) {
|
|
1658
|
-
let maxClaims = 0;
|
|
1659
|
-
for (const cond of conditions) {
|
|
1660
|
-
const max = getMaxClaimsForDynamicCondition(dynamicObj, cond);
|
|
1661
|
-
if (max === Infinity)
|
|
1662
|
-
return Infinity;
|
|
1663
|
-
maxClaims = Math.max(maxClaims, max);
|
|
1664
|
-
}
|
|
1665
|
-
return maxClaims;
|
|
1666
|
-
}
|
|
1667
|
-
// mixed:
|
|
1668
|
-
const maxIterations = 100;
|
|
1669
|
-
for (let n = currentClaimCount + 1; n <= currentClaimCount + maxIterations; n++) {
|
|
1670
|
-
if (!meetsDynamicConditions(dynamicObj, dynamicGroup, n)) {
|
|
1671
|
-
return n - 1;
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
return currentClaimCount + maxIterations;
|
|
1675
|
-
}
|
|
1676
|
-
/**
|
|
1677
|
-
* Evaluates a group of dynamic conditions with logical links (AND, OR, AND NOT).
|
|
1678
|
-
* @param dynamicObj - The player's dynamic object with any key and string or number value.
|
|
1679
|
-
* @param dynamicGroup - The group of conditions and links to check.
|
|
1680
|
-
* @param claimMultiplier - Multiplier to scale conditions (used for numeric comparisons).
|
|
1681
|
-
* @returns true if the group evaluates to true, false otherwise.
|
|
1682
|
-
*/
|
|
1683
|
-
function meetsDynamicConditions(dynamicObj, dynamicGroup, claimMultiplier = 1) {
|
|
1684
|
-
const { conditions, links } = dynamicGroup;
|
|
1685
|
-
if (!conditions || conditions.length === 0)
|
|
1686
|
-
return true;
|
|
1687
|
-
if (!dynamicObj)
|
|
1688
|
-
return false;
|
|
1689
|
-
// If no links, treat as AND between all conditions
|
|
1690
|
-
if (!links || links.length === 0) {
|
|
1691
|
-
return conditions.every((cond) => evaluateDynamicCondition(dynamicObj, cond, claimMultiplier));
|
|
1692
|
-
}
|
|
1693
|
-
// Evaluate the first condition
|
|
1694
|
-
let result = evaluateDynamicCondition(dynamicObj, conditions[0], claimMultiplier);
|
|
1695
|
-
for (let i = 0; i < links.length; i++) {
|
|
1696
|
-
const nextCond = evaluateDynamicCondition(dynamicObj, conditions[i + 1], claimMultiplier);
|
|
1697
|
-
const link = links[i];
|
|
1698
|
-
if (link === 'AND') {
|
|
1699
|
-
result = result && nextCond;
|
|
1700
|
-
}
|
|
1701
|
-
else if (link === 'OR') {
|
|
1702
|
-
result = result || nextCond;
|
|
1703
|
-
}
|
|
1704
|
-
else if (link === 'AND NOT') {
|
|
1705
|
-
result = result && !nextCond;
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
return result;
|
|
1709
|
-
}
|
|
1710
|
-
/**
|
|
1711
|
-
* Checks if a PlayerOffer meets its claimable conditions (completed -> claimable transition).
|
|
1712
|
-
* @param claimableConditions - The offer's claimableConditions (from IOffer)
|
|
1713
|
-
* @param claimableTrackers - The player offer's claimableTrackers
|
|
1714
|
-
*/
|
|
1715
|
-
function meetsClaimableConditions({ claimableConditions, playerOfferTrackers, claimableTrackers, }) {
|
|
1716
|
-
if (!claimableConditions) {
|
|
1717
|
-
return { isValid: true };
|
|
1718
|
-
}
|
|
1719
|
-
if (claimableConditions.siblingCompletions) {
|
|
1720
|
-
const siblingCount = playerOfferTrackers?.siblingPlayerOffer_ids?.length ?? 0;
|
|
1721
|
-
let completedCount = claimableTrackers?.siblingCompletions ?? 0;
|
|
1722
|
-
if (completedCount == -1)
|
|
1723
|
-
completedCount = siblingCount; // treat -1 as all completed
|
|
1724
|
-
// if siblings exist but not all are completed, return false
|
|
1725
|
-
if (siblingCount > 0 && completedCount < siblingCount) {
|
|
1726
|
-
return { isValid: false };
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
return { isValid: true };
|
|
1730
|
-
}
|
|
1731
|
-
// returns contractAddress:network -> balance
|
|
1732
|
-
function aggregateTokenBalances(data) {
|
|
1733
|
-
const aggregatedBalances = {};
|
|
1734
|
-
for (const { balances } of data?.cryptoWallets || []) {
|
|
1735
|
-
if (!balances)
|
|
1736
|
-
continue;
|
|
1737
|
-
for (const [key, balance] of Object.entries(balances)) {
|
|
1738
|
-
if (!aggregatedBalances[key]) {
|
|
1739
|
-
aggregatedBalances[key] = 0;
|
|
1740
|
-
}
|
|
1741
|
-
aggregatedBalances[key] += balance;
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
return aggregatedBalances;
|
|
1745
|
-
}
|
|
1746
|
-
//# sourceMappingURL=conditions.js.map
|