react-achievements 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,19 +27,14 @@ import {
27
27
  BadgesModal
28
28
  } from 'react-achievements';
29
29
 
30
- // Define achievements with the new Simple API - 90% less code!
31
- const achievements = {
32
- score: {
33
- 100: { title: 'Century!', description: 'Score 100 points', icon: '🏆' },
34
- 500: { title: 'High Scorer!', description: 'Score 500 points', icon: '⭐' }
35
- },
36
- level: {
37
- 5: { title: 'Leveling Up', description: 'Reach level 5', icon: '📈' }
38
- },
39
- completedTutorial: {
40
- true: { title: 'Tutorial Master', description: 'Complete the tutorial', icon: '📚' }
41
- }
42
- };
30
+ // Define achievements with the new three-tier Builder API - 95% less code!
31
+ import { AchievementBuilder } from 'react-achievements';
32
+
33
+ const gameAchievements = AchievementBuilder.combine([
34
+ AchievementBuilder.createScoreAchievements([100, 500]), // Smart defaults
35
+ AchievementBuilder.createLevelAchievement(5).withAward({ title: 'Leveling Up', icon: '📈' }), // Chainable customization
36
+ AchievementBuilder.createBooleanAchievement('completedTutorial').withAward({ title: 'Tutorial Master', icon: '📚' })
37
+ ]);
43
38
 
44
39
  // Demo component with all essential features
45
40
  const DemoComponent = () => {
@@ -93,7 +88,7 @@ const DemoComponent = () => {
93
88
  const App = () => {
94
89
  return (
95
90
  <AchievementProvider
96
- achievements={achievements}
91
+ achievements={gameAchievements}
97
92
  storage="local"
98
93
  >
99
94
  <DemoComponent />
@@ -110,44 +105,178 @@ When you click "Score 100 points":
110
105
  3. The achievement is stored and visible in the badges modal
111
106
  4. The badges button updates to show the new count
112
107
 
113
- ## 🚀 New Simple API
108
+ ## Simple API (Recommended)
114
109
 
115
- The Simple API reduces configuration complexity by **90%** while maintaining full backward compatibility:
110
+ Perfect for 90% of use cases - threshold-based achievements with minimal configuration:
116
111
 
117
- ### Before (Complex API)
118
112
  ```tsx
119
- const achievementConfig = {
120
- score: [{
121
- isConditionMet: (value: AchievementMetricArrayValue, state: AchievementState) => {
122
- const numValue = Array.isArray(value) ? value[0] : value;
123
- return typeof numValue === 'number' && numValue >= 100;
124
- },
125
- achievementDetails: {
126
- achievementId: 'score_100',
127
- achievementTitle: 'Century!',
128
- achievementDescription: 'Score 100 points',
129
- achievementIconKey: 'trophy'
130
- }
131
- }]
132
- };
133
- ```
113
+ import { AchievementProvider, useSimpleAchievements } from 'react-achievements';
134
114
 
135
- ### After (Simple API)
136
- ```tsx
137
115
  const achievements = {
116
+ // Numeric thresholds
138
117
  score: {
139
- 100: { title: 'Century!', description: 'Score 100 points', icon: '🏆' }
118
+ 100: { title: 'Century!', description: 'Score 100 points', icon: '🏆' },
119
+ 500: { title: 'High Scorer!', icon: '⭐' }
120
+ },
121
+
122
+ // Boolean achievements
123
+ completedTutorial: {
124
+ true: { title: 'Tutorial Master', description: 'Complete the tutorial', icon: '📚' }
125
+ },
126
+
127
+ // String-based achievements
128
+ characterClass: {
129
+ wizard: { title: 'Arcane Scholar', description: 'Choose the wizard class', icon: '🧙‍♂️' },
130
+ warrior: { title: 'Battle Hardened', description: 'Choose the warrior class', icon: '⚔️' }
131
+ },
132
+
133
+ // Custom condition functions for complex logic
134
+ combo: {
135
+ custom: {
136
+ title: 'Perfect Combo',
137
+ description: 'Score 1000+ with 100% accuracy',
138
+ icon: '💎',
139
+ condition: (metrics) => metrics.score >= 1000 && metrics.accuracy === 100
140
+ }
140
141
  }
141
142
  };
143
+
144
+ const { track, unlocked, unlockedCount, reset } = useSimpleAchievements();
145
+
146
+ // Track achievements easily
147
+ track('score', 100); // Unlocks "Century!" achievement
148
+ track('completedTutorial', true); // Unlocks "Tutorial Master"
149
+ track('characterClass', 'wizard'); // Unlocks "Arcane Scholar"
150
+
151
+ // Track multiple metrics for custom conditions
152
+ track('score', 1000);
153
+ track('accuracy', 100); // Unlocks "Perfect Combo" if both conditions met
154
+ ```
155
+
156
+ ### Simple API Comparison Logic
157
+
158
+ When using the Simple API, achievement conditions use different comparison operators depending on the value type:
159
+
160
+ | Value Type | Comparison | Example | When Achievement Unlocks |
161
+ |------------|------------|---------|-------------------------|
162
+ | **Numeric** | `>=` (greater than or equal) | `score: { 100: {...} }` | When `track('score', 100)` or higher |
163
+ | **Boolean** | `===` (strict equality) | `completedTutorial: { true: {...} }` | When `track('completedTutorial', true)` |
164
+ | **String** | `===` (strict equality) | `characterClass: { wizard: {...} }` | When `track('characterClass', 'wizard')` |
165
+
166
+ **Important Notes:**
167
+ - **Numeric achievements** use `>=` comparison, so they unlock when you reach **or exceed** the threshold
168
+ - **Boolean and string achievements** use exact equality matching
169
+ - Custom condition functions have full control over comparison logic
170
+
171
+ **Examples:**
172
+ ```tsx
173
+ // Numeric: Achievement unlocks at 100 or higher
174
+ track('score', 150); // ✅ Unlocks "Century!" (threshold: 100)
175
+ track('score', 99); // ❌ Does not unlock
176
+
177
+ // Boolean: Must match exactly
178
+ track('completedTutorial', true); // ✅ Unlocks achievement
179
+ track('completedTutorial', false); // ❌ Does not unlock
180
+
181
+ // String: Must match exactly
182
+ track('characterClass', 'wizard'); // ✅ Unlocks "Arcane Scholar"
183
+ track('characterClass', 'Wizard'); // ❌ Does not unlock (case sensitive)
184
+ ```
185
+
186
+ ## Three-Tier Builder API
187
+
188
+ The AchievementBuilder provides three levels of complexity to match your needs - from zero-config defaults to full custom logic:
189
+
190
+ ### Tier 1: Smart Defaults (90% of use cases)
191
+
192
+ Zero configuration needed - just specify what you want to track:
193
+
194
+ ```tsx
195
+ import { AchievementBuilder } from 'react-achievements';
196
+
197
+ // Individual achievements with smart defaults
198
+ AchievementBuilder.createScoreAchievement(100); // "Score 100!" + 🏆
199
+ AchievementBuilder.createLevelAchievement(5); // "Level 5!" + 📈
200
+ AchievementBuilder.createBooleanAchievement('completedTutorial'); // "Completed Tutorial!" + ✅
201
+
202
+ // Bulk creation with smart defaults
203
+ AchievementBuilder.createScoreAchievements([100, 500, 1000]);
204
+ AchievementBuilder.createLevelAchievements([5, 10, 25]);
205
+
206
+ // Mixed: some defaults, some custom awards
207
+ const achievements = AchievementBuilder.createScoreAchievements([
208
+ 100, // Uses default "Score 100!" + 🏆
209
+ [500, { title: 'High Scorer!', icon: '⭐' }], // Custom award
210
+ 1000 // Uses default "Score 1000!" + 🏆
211
+ ]);
212
+ ```
213
+
214
+ ### Tier 2: Chainable Customization
215
+
216
+ Start with defaults, then customize awards as needed:
217
+
218
+ ```tsx
219
+ // Individual achievements with custom awards
220
+ const achievements = AchievementBuilder.combine([
221
+ AchievementBuilder.createScoreAchievement(100)
222
+ .withAward({ title: 'Century!', description: 'Amazing score!', icon: '🏆' }),
223
+
224
+ AchievementBuilder.createLevelAchievement(5)
225
+ .withAward({ title: 'Getting Started', icon: '🌱' }),
226
+
227
+ AchievementBuilder.createBooleanAchievement('completedTutorial')
228
+ .withAward({ title: 'Tutorial Master', description: 'You did it!', icon: '📚' }),
229
+
230
+ AchievementBuilder.createValueAchievement('characterClass', 'wizard')
231
+ .withAward({ title: 'Arcane Scholar', icon: '🧙‍♂️' })
232
+ ]);
233
+ ```
234
+
235
+ ### Tier 3: Full Control for Complex Logic
236
+
237
+ Complete control over achievement conditions for power users:
238
+
239
+ ```tsx
240
+ // Handle complex scenarios like Date, null, undefined values
241
+ const complexAchievement = AchievementBuilder.create()
242
+ .withId('weekly_login')
243
+ .withMetric('lastLoginDate')
244
+ .withCondition((value, state) => {
245
+ // Handle all possible value types
246
+ if (value === null || value === undefined) return false;
247
+ if (value instanceof Date) {
248
+ return value.getTime() > Date.now() - (7 * 24 * 60 * 60 * 1000);
249
+ }
250
+ return false;
251
+ })
252
+ .withAward({
253
+ title: 'Weekly Warrior',
254
+ description: 'Logged in within the last week',
255
+ icon: '📅'
256
+ })
257
+ .build();
258
+
259
+ // Multiple complex achievements
260
+ const advancedAchievements = AchievementBuilder.combine([
261
+ complexAchievement,
262
+ AchievementBuilder.create()
263
+ .withId('perfect_combo')
264
+ .withMetric('gameState')
265
+ .withCondition((value, state) => {
266
+ return state.score >= 1000 && state.accuracy === 100;
267
+ })
268
+ .withAward({ title: 'Perfect!', icon: '💎' })
269
+ .build()
270
+ ]);
142
271
  ```
143
272
 
144
273
  ### Key Benefits
145
- - **90% less configuration code** for common use cases
146
- - **Threshold-based achievements** work automatically
147
- - **Custom condition functions** for complex scenarios
148
- - **Automatic ID generation** from metric names and thresholds
149
- - **Built-in emoji support** - no more icon key mapping
150
- - **Full backward compatibility** - existing code continues to work
274
+ - **Progressive complexity**: Start simple, add complexity only when needed
275
+ - **Zero configuration**: Works out of the box with smart defaults
276
+ - **Chainable customization**: Fine-tune awards without changing logic
277
+ - **Type-safe**: Full TypeScript support for complex conditions
278
+ - **Handles edge cases**: Date, null, undefined values in Tier 3
279
+ - **Combinable**: Mix and match different tiers in one configuration
151
280
 
152
281
  ## State Management Options
153
282
 
@@ -204,88 +333,23 @@ To allow users to view their achievement history, the package provides two essen
204
333
 
205
334
  These components are the recommended way to give users access to their achievement history. While you could build custom UI using the `useAchievements` hook data, these components provide a polished, ready-to-use interface for achievement history.
206
335
 
207
- ## API Options
208
-
209
- ### Simple API (Recommended)
210
- Perfect for 90% of use cases - threshold-based achievements with minimal configuration:
211
-
212
- ```tsx
213
- import { AchievementProvider, useSimpleAchievements } from 'react-achievements';
214
-
215
- const achievements = {
216
- // Numeric thresholds
217
- score: {
218
- 100: { title: 'Century!', description: 'Score 100 points', icon: '🏆' },
219
- 500: { title: 'High Scorer!', icon: '⭐' }
220
- },
221
-
222
- // Boolean achievements
223
- completedTutorial: {
224
- true: { title: 'Tutorial Master', description: 'Complete the tutorial', icon: '📚' }
225
- },
226
-
227
- // String-based achievements
228
- characterClass: {
229
- wizard: { title: 'Arcane Scholar', description: 'Choose the wizard class', icon: '🧙‍♂️ ' },
230
- warrior: { title: 'Battle Hardened', description: 'Choose the warrior class', icon: '⚔️' }
231
- },
232
-
233
- // Custom condition functions for complex logic
234
- combo: {
235
- custom: {
236
- title: 'Perfect Combo',
237
- description: 'Score 1000+ with 100% accuracy',
238
- icon: '💎',
239
- condition: (metrics) => metrics.score >= 1000 && metrics.accuracy === 100
240
- }
241
- }
242
- };
243
-
244
- const { track, unlocked, unlockedCount, reset } = useSimpleAchievements();
245
-
246
- // Track achievements easily
247
- track('score', 100); // Unlocks "Century!" achievement
248
- track('completedTutorial', true); // Unlocks "Tutorial Master"
249
- track('characterClass', 'wizard'); // Unlocks "Arcane Scholar"
250
336
 
251
- // Track multiple metrics for custom conditions
252
- track('score', 1000);
253
- track('accuracy', 100); // Unlocks "Perfect Combo" if both conditions met
254
- ```
337
+ ## Default Icons
255
338
 
256
- ### Complex API (Advanced)
257
- For complex scenarios requiring full control:
339
+ The package comes with a comprehensive set of default icons that you can use in your achievements. These are available through the `defaultAchievementIcons` export:
258
340
 
259
341
  ```tsx
260
- import { AchievementProvider, useAchievements } from 'react-achievements';
342
+ import { AchievementProvider } from 'react-achievements';
261
343
 
262
- // Define your achievements using the traditional complex format
344
+ // Example achievement configuration using direct emoji icons
263
345
  const achievements = {
264
- score: [{
265
- isConditionMet: (value: AchievementMetricArrayValue, state: AchievementState) => {
266
- const numValue = Array.isArray(value) ? value[0] : value;
267
- return typeof numValue === 'number' && numValue >= 100;
268
- },
269
- achievementDetails: {
270
- achievementId: 'score_100',
271
- achievementTitle: 'Century!',
272
- achievementDescription: 'Score 100 points',
273
- achievementIconKey: 'trophy'
346
+ pageViews: {
347
+ 5: {
348
+ title: 'Getting Started',
349
+ description: 'Viewed 5 pages',
350
+ icon: '👣'
274
351
  }
275
- }],
276
-
277
- completedTutorial: [{
278
- isConditionMet: (value: AchievementMetricArrayValue, state: AchievementState) => {
279
- const boolValue = Array.isArray(value) ? value[0] : value;
280
- return typeof boolValue === 'boolean' && boolValue === true;
281
- },
282
- achievementDetails: {
283
- achievementId: 'tutorial_complete',
284
- achievementTitle: 'Tutorial Master',
285
- achievementDescription: 'Complete the tutorial',
286
- achievementIconKey: 'book'
287
- }
288
- }]
352
+ }
289
353
  };
290
354
 
291
355
  // Create your app component
@@ -293,61 +357,7 @@ const App = () => {
293
357
  return (
294
358
  <AchievementProvider
295
359
  achievements={achievements}
296
- storage="local" // or "memory" or custom storage
297
- >
298
- <Game />
299
- </AchievementProvider>
300
- );
301
- };
302
-
303
- // Use achievements in your components
304
- const Game = () => {
305
- const { update, achievements } = useAchievements();
306
-
307
- const handleScoreUpdate = (newScore: number) => {
308
- update({ score: newScore });
309
- };
310
-
311
- return (
312
- <div>
313
- <h1>Game</h1>
314
- <p>Unlocked Achievements: {achievements.unlocked.length}</p>
315
- <button onClick={() => handleScoreUpdate(100)}>
316
- Score 100 points
317
- </button>
318
- </div>
319
- );
320
- };
321
- ```
322
-
323
- ## Default Icons
324
-
325
- The package comes with a comprehensive set of default icons that you can use in your achievements. These are available through the `defaultAchievementIcons` export:
326
-
327
- ```tsx
328
- import { AchievementProvider, defaultAchievementIcons } from 'react-achievements-core';
329
-
330
- // Example achievement configuration using default icons
331
- const achievementConfig = {
332
- pageViews: [
333
- {
334
- isConditionMet: (value) => value >= 5,
335
- achievementDetails: {
336
- achievementId: 'views-5',
337
- achievementTitle: 'Getting Started',
338
- achievementDescription: 'Viewed 5 pages',
339
- achievementIconKey: 'firstStep' // This will use the 👣 emoji from defaultAchievementIcons
340
- }
341
- }
342
- ]
343
- };
344
-
345
- // Create your app component
346
- const App = () => {
347
- return (
348
- <AchievementProvider
349
- achievements={achievementConfig}
350
- // The provider automatically uses defaultAchievementIcons
360
+ storage="local"
351
361
  >
352
362
  <Game />
353
363
  </AchievementProvider>
@@ -355,111 +365,101 @@ const App = () => {
355
365
  };
356
366
  ```
357
367
 
358
- ### Custom Icons
368
+ ### Using Icons
359
369
 
360
- You can also provide your own custom icons that will override or extend the default ones:
370
+ The Simple API makes icon usage straightforward - just include emojis directly in your achievement definitions:
361
371
 
362
372
  ```tsx
363
- import { AchievementProvider, defaultAchievementIcons } from 'react-achievements-core';
364
-
365
- // Create custom icons by extending the defaults
366
- const customIcons = {
367
- ...defaultAchievementIcons, // Include all default icons
368
- levelUp: '🚀', // Override the default for 'levelUp'
369
- myCustomIcon: '💻' // Add a new icon not in the defaults
370
- };
371
-
372
- const App = () => {
373
- return (
374
- <AchievementProvider
375
- achievements={achievementConfig}
376
- icons={customIcons} // Pass your custom icons to the provider
377
- >
378
- <Game />
379
- </AchievementProvider>
380
- );
373
+ const achievements = {
374
+ score: {
375
+ 100: { title: 'Century!', icon: '🏆' },
376
+ 500: { title: 'High Scorer!', icon: '⭐' },
377
+ 1000: { title: 'Elite Player!', icon: '💎' }
378
+ },
379
+ level: {
380
+ 5: { title: 'Getting Started', icon: '🌱' },
381
+ 10: { title: 'Rising Star', icon: '🚀' },
382
+ 25: { title: 'Expert', icon: '👑' }
383
+ }
381
384
  };
382
385
  ```
383
386
 
384
- ### Available Icons
387
+ ### Fallback Icons
385
388
 
386
- The `defaultAchievementIcons` includes icons in these categories:
387
-
388
- - General Progress & Milestones (levelUp, questComplete, etc.)
389
- - Social & Engagement (shared, liked, etc.)
390
- - Time & Activity (activeDay, streak, etc.)
391
- - Creativity & Skill (artist, expert, etc.)
392
- - Achievement Types (bronze, silver, gold, etc.)
393
- - Numbers & Counters (one, ten, hundred, etc.)
394
- - Actions & Interactions (clicked, discovered, etc.)
395
- - Placeholders (default, loading, error, etc.)
396
- - Miscellaneous (trophy, star, gem, etc.)
389
+ The library provides a small set of essential fallback icons for system use (error states, loading, etc.). These are automatically used when needed and don't require any configuration.
397
390
 
398
391
  ## Custom Storage
399
392
 
400
393
  You can implement your own storage solution by implementing the `AchievementStorage` interface:
401
394
 
402
- ```typescript
403
- import { AchievementStorage } from 'react-achievements-core';
395
+ ```tsx
396
+ import { AchievementStorage, AchievementMetrics, AchievementProvider } from 'react-achievements';
404
397
 
405
398
  class CustomStorage implements AchievementStorage {
406
- getMetrics() {
399
+ getMetrics(): AchievementMetrics {
407
400
  // Your implementation
401
+ return {};
408
402
  }
409
403
 
410
- setMetrics(metrics) {
404
+ setMetrics(metrics: AchievementMetrics): void {
411
405
  // Your implementation
412
406
  }
413
407
 
414
- getUnlockedAchievements() {
408
+ getUnlockedAchievements(): string[] {
415
409
  // Your implementation
410
+ return [];
416
411
  }
417
412
 
418
- setUnlockedAchievements(achievements) {
413
+ setUnlockedAchievements(achievements: string[]): void {
419
414
  // Your implementation
420
415
  }
421
416
 
422
- clear() {
417
+ clear(): void {
423
418
  // Your implementation
424
419
  }
425
420
  }
426
421
 
427
422
  // Use your custom storage
423
+ const gameAchievements = {
424
+ score: {
425
+ 100: { title: 'Century!', icon: '🏆' }
426
+ }
427
+ };
428
+
428
429
  const App = () => {
429
- return (
430
- <AchievementProvider
431
- achievements={achievements}
432
- storage={new CustomStorage()}
433
- >
434
- <Game />
435
- </AchievementProvider>
436
- );
430
+ return (
431
+ <AchievementProvider
432
+ achievements={gameAchievements}
433
+ storage={new CustomStorage()} // Use your custom storage implementation
434
+ >
435
+ </AchievementProvider>
436
+ );
437
437
  };
438
+
439
+ export default App;
438
440
  ```
439
441
 
440
442
  ## Styling
441
443
 
442
- You can customize the appearance of the achievement components:
444
+ The achievement components use default styling that works well out of the box. For custom styling, you can override the CSS classes or customize individual component props:
443
445
 
444
446
  ```tsx
445
- const App = () => {
446
- return (
447
- <AchievementProvider
448
- achievements={achievements}
449
- theme={{
450
- colors: {
451
- primary: '#ff0000',
452
- background: '#f0f0f0'
453
- },
454
- position: 'top-right'
455
- }}
456
- >
457
- <Game />
458
- </AchievementProvider>
459
- );
460
- };
447
+ // Individual component styling
448
+ <BadgesButton
449
+ position="bottom-right"
450
+ style={{ backgroundColor: '#ff0000' }}
451
+ unlockedAchievements={achievements.unlocked}
452
+ />
453
+
454
+ <BadgesModal
455
+ isOpen={isModalOpen}
456
+ onClose={() => setIsModalOpen(false)}
457
+ achievements={achievements.unlocked}
458
+ style={{ backgroundColor: '#f0f0f0' }}
459
+ />
461
460
  ```
462
461
 
462
+
463
463
  ## API Reference
464
464
 
465
465
  ### AchievementProvider Props
@@ -479,6 +479,76 @@ Returns an object with:
479
479
  - `achievements`: Object containing unlocked and locked achievements
480
480
  - `reset`: Function to reset achievement storage
481
481
 
482
+ ## Advanced: Complex API
483
+
484
+ For complex scenarios requiring full control over achievement logic, you can use the traditional Complex API with POJO (Plain Old JavaScript Object) configurations:
485
+
486
+ ```tsx
487
+ import { AchievementProvider, useAchievements } from 'react-achievements';
488
+
489
+ // Define your achievements using the traditional complex format
490
+ const achievements = {
491
+ score: [{
492
+ isConditionMet: (value: AchievementMetricArrayValue, state: AchievementState) => {
493
+ const numValue = Array.isArray(value) ? value[0] : value;
494
+ return typeof numValue === 'number' && numValue >= 100;
495
+ },
496
+ achievementDetails: {
497
+ achievementId: 'score_100',
498
+ achievementTitle: 'Century!',
499
+ achievementDescription: 'Score 100 points',
500
+ achievementIconKey: 'trophy'
501
+ }
502
+ }],
503
+
504
+ completedTutorial: [{
505
+ isConditionMet: (value: AchievementMetricArrayValue, state: AchievementState) => {
506
+ const boolValue = Array.isArray(value) ? value[0] : value;
507
+ return typeof boolValue === 'boolean' && boolValue === true;
508
+ },
509
+ achievementDetails: {
510
+ achievementId: 'tutorial_complete',
511
+ achievementTitle: 'Tutorial Master',
512
+ achievementDescription: 'Complete the tutorial',
513
+ achievementIconKey: 'book'
514
+ }
515
+ }]
516
+ };
517
+
518
+ // Create your app component
519
+ const App = () => {
520
+ return (
521
+ <AchievementProvider
522
+ achievements={achievements}
523
+ storage="local" // or "memory" or custom storage
524
+ >
525
+ <Game />
526
+ </AchievementProvider>
527
+ );
528
+ };
529
+
530
+ // Use achievements in your components
531
+ const Game = () => {
532
+ const { update, achievements } = useAchievements();
533
+
534
+ const handleScoreUpdate = (newScore: number) => {
535
+ update({ score: newScore });
536
+ };
537
+
538
+ return (
539
+ <div>
540
+ <h1>Game</h1>
541
+ <p>Unlocked Achievements: {achievements.unlocked.length}</p>
542
+ <button onClick={() => handleScoreUpdate(100)}>
543
+ Score 100 points
544
+ </button>
545
+ </div>
546
+ );
547
+ };
548
+ ```
549
+
550
+ This API provides maximum flexibility for complex achievement logic but requires more verbose configuration. Most users should use the Simple API or Builder API instead.
551
+
482
552
  ## License
483
553
 
484
554
  MIT