@umang-boss/claudemon 2.1.3 → 2.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.
@@ -1,36 +1,41 @@
1
1
  /**
2
2
  * Legendary quest chain definitions for Claudemon.
3
3
  * Each legendary Pokemon requires completing a multi-step quest
4
- * tied to sustained coding activity.
4
+ * tied to sustained coding activity. Designed for corporate developers —
5
+ * tough but achievable with consistent daily work.
5
6
  */
6
7
 
7
8
  import type { AchievementCondition, LegendaryQuest, PlayerState } from "../engine/types.js";
9
+ import { POKEMON_BY_ID } from "../engine/pokemon-data.js";
8
10
  import { isConditionMet } from "./achievements.js";
9
11
 
10
12
  // ── Quest Definitions ────────────────────────────────────────
11
13
 
12
14
  export const LEGENDARY_QUESTS: readonly LegendaryQuest[] = [
15
+ // ═══════════════════════════════════════════════════════════
16
+ // GEN 1 — Kanto Legends
17
+ // ═══════════════════════════════════════════════════════════
18
+
13
19
  // ── Articuno (#144) — "The Ice Bird of Endurance" ────────
14
20
  {
15
21
  pokemonId: 144,
16
22
  name: "The Ice Bird of Endurance",
17
23
  steps: [
18
24
  {
19
- description: "Code for 30 days (weekends off OK)",
20
- condition: { type: "streak", minDays: 30 },
25
+ description: "Code for 14 days (weekends off OK)",
26
+ condition: { type: "streak", minDays: 14 },
21
27
  },
22
28
  {
23
29
  description: "Catch 3 Water or Ice type Pokemon",
24
- // Approximated as pokedex gate; real type-check in getQuestProgress.
25
30
  condition: { type: "pokedex", minCaught: 3 },
26
31
  },
27
32
  {
28
- description: "Code for 100 days (weekends off OK)",
29
- condition: { type: "streak", minDays: 100 },
33
+ description: "Code for 30 days (weekends off OK)",
34
+ condition: { type: "streak", minDays: 30 },
30
35
  },
31
36
  {
32
- description: "Reach level 50 on your active Pokemon",
33
- condition: { type: "level", minLevel: 50 },
37
+ description: "Reach level 30 on your active Pokemon",
38
+ condition: { type: "level", minLevel: 30 },
34
39
  },
35
40
  ],
36
41
  },
@@ -41,20 +46,20 @@ export const LEGENDARY_QUESTS: readonly LegendaryQuest[] = [
41
46
  name: "The Thunder of Testing",
42
47
  steps: [
43
48
  {
44
- description: "Pass 100 tests",
45
- condition: { type: "counter", counter: "tests_passed", threshold: 100 },
49
+ description: "Pass 50 tests",
50
+ condition: { type: "counter", counter: "tests_passed", threshold: 50 },
46
51
  },
47
52
  {
48
- description: "Write 50 test files",
49
- condition: { type: "counter", counter: "tests_written", threshold: 50 },
53
+ description: "Write 20 test files",
54
+ condition: { type: "counter", counter: "tests_written", threshold: 20 },
50
55
  },
51
56
  {
52
- description: "Pass 500 tests",
53
- condition: { type: "counter", counter: "tests_passed", threshold: 500 },
57
+ description: "Pass 200 tests",
58
+ condition: { type: "counter", counter: "tests_passed", threshold: 200 },
54
59
  },
55
60
  {
56
- description: "Pass 1000 tests lifetime",
57
- condition: { type: "counter", counter: "tests_passed", threshold: 1000 },
61
+ description: "Pass 500 tests lifetime",
62
+ condition: { type: "counter", counter: "tests_passed", threshold: 500 },
58
63
  },
59
64
  ],
60
65
  },
@@ -65,20 +70,20 @@ export const LEGENDARY_QUESTS: readonly LegendaryQuest[] = [
65
70
  name: "The Flame of Debugging",
66
71
  steps: [
67
72
  {
68
- description: "Fix 50 bugs",
69
- condition: { type: "counter", counter: "bugs_fixed", threshold: 50 },
73
+ description: "Fix 25 bugs",
74
+ condition: { type: "counter", counter: "bugs_fixed", threshold: 25 },
70
75
  },
71
76
  {
72
77
  description: "Earn the Blaze Badge",
73
78
  condition: { type: "badge", badge: "blaze" },
74
79
  },
75
80
  {
76
- description: "Fix 200 bugs",
77
- condition: { type: "counter", counter: "bugs_fixed", threshold: 200 },
81
+ description: "Fix 100 bugs",
82
+ condition: { type: "counter", counter: "bugs_fixed", threshold: 100 },
78
83
  },
79
84
  {
80
- description: "Fix 500 bugs lifetime",
81
- condition: { type: "counter", counter: "bugs_fixed", threshold: 500 },
85
+ description: "Fix 250 bugs lifetime",
86
+ condition: { type: "counter", counter: "bugs_fixed", threshold: 250 },
82
87
  },
83
88
  ],
84
89
  },
@@ -89,20 +94,20 @@ export const LEGENDARY_QUESTS: readonly LegendaryQuest[] = [
89
94
  name: "The Ultimate Creation",
90
95
  steps: [
91
96
  {
92
- description: "Catch 50 unique Pokemon",
93
- condition: { type: "pokedex", minCaught: 50 },
97
+ description: "Catch 30 unique Pokemon",
98
+ condition: { type: "pokedex", minCaught: 30 },
94
99
  },
95
100
  {
96
- description: "Reach level 75 on any Pokemon",
97
- condition: { type: "level", minLevel: 75 },
101
+ description: "Reach level 40 on any Pokemon",
102
+ condition: { type: "level", minLevel: 40 },
98
103
  },
99
104
  {
100
- description: "Catch 100 unique Pokemon",
101
- condition: { type: "pokedex", minCaught: 100 },
105
+ description: "Catch 75 unique Pokemon",
106
+ condition: { type: "pokedex", minCaught: 75 },
102
107
  },
103
108
  {
104
- description: "Catch 140 unique Pokemon",
105
- condition: { type: "pokedex", minCaught: 140 },
109
+ description: "Catch 120 unique Pokemon",
110
+ condition: { type: "pokedex", minCaught: 120 },
106
111
  },
107
112
  ],
108
113
  },
@@ -113,24 +118,456 @@ export const LEGENDARY_QUESTS: readonly LegendaryQuest[] = [
113
118
  name: "The Myth",
114
119
  steps: [
115
120
  {
116
- description: "Code for 100 days (weekends off OK)",
117
- condition: { type: "streak", minDays: 100 },
121
+ description: "Code for 60 days (weekends off OK)",
122
+ condition: { type: "streak", minDays: 60 },
118
123
  },
119
124
  {
120
125
  description: "Catch all 3 legendary birds (Articuno, Zapdos, Moltres)",
121
- // Gate: must have at least 148 caught (birds are #144-146).
122
- // Real check in getQuestProgress verifies specific IDs.
123
- condition: { type: "pokedex", minCaught: 148 },
126
+ condition: { type: "pokedex", minCaught: 50 },
124
127
  },
125
128
  {
126
129
  description: "Catch Mewtwo",
127
- // Gate: must have at least 149 caught (Mewtwo is #150).
128
- // Real check in getQuestProgress verifies specific ID.
129
- condition: { type: "pokedex", minCaught: 149 },
130
+ condition: { type: "pokedex", minCaught: 60 },
131
+ },
132
+ {
133
+ description: "Code for 180 days (weekends off OK)",
134
+ condition: { type: "streak", minDays: 180 },
135
+ },
136
+ ],
137
+ },
138
+
139
+ // ═══════════════════════════════════════════════════════════
140
+ // GEN 2 — Johto Legends
141
+ // ═══════════════════════════════════════════════════════════
142
+
143
+ // ── Lugia (#249) — "The Guardian of the Deep" ───────────
144
+ {
145
+ pokemonId: 249,
146
+ name: "The Guardian of the Deep",
147
+ steps: [
148
+ {
149
+ description: "Pass 100 tests",
150
+ condition: { type: "counter", counter: "tests_passed", threshold: 100 },
151
+ },
152
+ {
153
+ description: "Build successfully 50 times",
154
+ condition: { type: "counter", counter: "builds_succeeded", threshold: 50 },
155
+ },
156
+ {
157
+ description: "Catch 40 unique Pokemon",
158
+ condition: { type: "pokedex", minCaught: 40 },
159
+ },
160
+ {
161
+ description: "Code for 45 days (weekends off OK)",
162
+ condition: { type: "streak", minDays: 45 },
163
+ },
164
+ ],
165
+ },
166
+
167
+ // ── Ho-Oh (#250) — "The Rainbow Phoenix" ────────────────
168
+ {
169
+ pokemonId: 250,
170
+ name: "The Rainbow Phoenix",
171
+ steps: [
172
+ {
173
+ description: "Catch Pokemon of 10 different types",
174
+ condition: { type: "pokedex", minCaught: 20 },
175
+ },
176
+ {
177
+ description: "Make 100 commits",
178
+ condition: { type: "counter", counter: "commits", threshold: 100 },
179
+ },
180
+ {
181
+ description: "Reach level 35 on any Pokemon",
182
+ condition: { type: "level", minLevel: 35 },
183
+ },
184
+ {
185
+ description: "Code for 60 days (weekends off OK)",
186
+ condition: { type: "streak", minDays: 60 },
187
+ },
188
+ ],
189
+ },
190
+
191
+ // ═══════════════════════════════════════════════════════════
192
+ // GEN 3 — Hoenn Legends
193
+ // ═══════════════════════════════════════════════════════════
194
+
195
+ // ── Kyogre (#382) — "The Ocean Architect" ───────────────
196
+ {
197
+ pokemonId: 382,
198
+ name: "The Ocean Architect",
199
+ steps: [
200
+ {
201
+ description: "Create 50 files",
202
+ condition: { type: "counter", counter: "files_created", threshold: 50 },
203
+ },
204
+ {
205
+ description: "Pass 150 tests",
206
+ condition: { type: "counter", counter: "tests_passed", threshold: 150 },
207
+ },
208
+ {
209
+ description: "Earn the Flow Badge",
210
+ condition: { type: "badge", badge: "flow" },
211
+ },
212
+ {
213
+ description: "Catch 50 unique Pokemon",
214
+ condition: { type: "pokedex", minCaught: 50 },
215
+ },
216
+ ],
217
+ },
218
+
219
+ // ── Groudon (#383) — "The Continent Builder" ────────────
220
+ {
221
+ pokemonId: 383,
222
+ name: "The Continent Builder",
223
+ steps: [
224
+ {
225
+ description: "Edit 200 files",
226
+ condition: { type: "counter", counter: "files_edited", threshold: 200 },
227
+ },
228
+ {
229
+ description: "Build successfully 75 times",
230
+ condition: { type: "counter", counter: "builds_succeeded", threshold: 75 },
231
+ },
232
+ {
233
+ description: "Fix 75 bugs",
234
+ condition: { type: "counter", counter: "bugs_fixed", threshold: 75 },
235
+ },
236
+ {
237
+ description: "Code for 45 days (weekends off OK)",
238
+ condition: { type: "streak", minDays: 45 },
239
+ },
240
+ ],
241
+ },
242
+
243
+ // ── Rayquaza (#384) — "The Sky Sovereign" ───────────────
244
+ {
245
+ pokemonId: 384,
246
+ name: "The Sky Sovereign",
247
+ steps: [
248
+ {
249
+ description: "Catch both Kyogre and Groudon",
250
+ condition: { type: "pokedex", minCaught: 60 },
251
+ },
252
+ {
253
+ description: "Make 200 commits",
254
+ condition: { type: "counter", counter: "commits", threshold: 200 },
255
+ },
256
+ {
257
+ description: "Reach level 45 on any Pokemon",
258
+ condition: { type: "level", minLevel: 45 },
259
+ },
260
+ {
261
+ description: "Code for 90 days (weekends off OK)",
262
+ condition: { type: "streak", minDays: 90 },
263
+ },
264
+ ],
265
+ },
266
+
267
+ // ═══════════════════════════════════════════════════════════
268
+ // GEN 4 — Sinnoh Legends
269
+ // ═══════════════════════════════════════════════════════════
270
+
271
+ // ── Dialga (#483) — "The Temporal Coder" ────────────────
272
+ {
273
+ pokemonId: 483,
274
+ name: "The Temporal Coder",
275
+ steps: [
276
+ {
277
+ description: "Code for 30 days (weekends off OK)",
278
+ condition: { type: "streak", minDays: 30 },
279
+ },
280
+ {
281
+ description: "Make 150 commits",
282
+ condition: { type: "counter", counter: "commits", threshold: 150 },
283
+ },
284
+ {
285
+ description: "Catch 60 unique Pokemon",
286
+ condition: { type: "pokedex", minCaught: 60 },
287
+ },
288
+ {
289
+ description: "Earn the Spark Badge",
290
+ condition: { type: "badge", badge: "spark" },
291
+ },
292
+ ],
293
+ },
294
+
295
+ // ── Palkia (#484) — "The Spatial Engineer" ──────────────
296
+ {
297
+ pokemonId: 484,
298
+ name: "The Spatial Engineer",
299
+ steps: [
300
+ {
301
+ description: "Create 75 files",
302
+ condition: { type: "counter", counter: "files_created", threshold: 75 },
303
+ },
304
+ {
305
+ description: "Edit 300 files",
306
+ condition: { type: "counter", counter: "files_edited", threshold: 300 },
307
+ },
308
+ {
309
+ description: "Pass 300 tests",
310
+ condition: { type: "counter", counter: "tests_passed", threshold: 300 },
311
+ },
312
+ {
313
+ description: "Code for 60 days (weekends off OK)",
314
+ condition: { type: "streak", minDays: 60 },
315
+ },
316
+ ],
317
+ },
318
+
319
+ // ── Giratina (#487) — "The Shadow Refactorer" ───────────
320
+ {
321
+ pokemonId: 487,
322
+ name: "The Shadow Refactorer",
323
+ steps: [
324
+ {
325
+ description: "Catch both Dialga and Palkia",
326
+ condition: { type: "pokedex", minCaught: 70 },
327
+ },
328
+ {
329
+ description: "Fix 150 bugs",
330
+ condition: { type: "counter", counter: "bugs_fixed", threshold: 150 },
331
+ },
332
+ {
333
+ description: "Perform 50 lint fixes",
334
+ condition: { type: "counter", counter: "lint_fixes", threshold: 50 },
335
+ },
336
+ {
337
+ description: "Code for 90 days (weekends off OK)",
338
+ condition: { type: "streak", minDays: 90 },
339
+ },
340
+ ],
341
+ },
342
+
343
+ // ═══════════════════════════════════════════════════════════
344
+ // GEN 5 — Unova Legends
345
+ // ═══════════════════════════════════════════════════════════
346
+
347
+ // ── Reshiram (#643) — "The Flame of Truth" ──────────────
348
+ {
349
+ pokemonId: 643,
350
+ name: "The Flame of Truth",
351
+ steps: [
352
+ {
353
+ description: "Write 30 test files",
354
+ condition: { type: "counter", counter: "tests_written", threshold: 30 },
355
+ },
356
+ {
357
+ description: "Pass 250 tests",
358
+ condition: { type: "counter", counter: "tests_passed", threshold: 250 },
359
+ },
360
+ {
361
+ description: "Reach level 40 on any Pokemon",
362
+ condition: { type: "level", minLevel: 40 },
363
+ },
364
+ {
365
+ description: "Code for 60 days (weekends off OK)",
366
+ condition: { type: "streak", minDays: 60 },
367
+ },
368
+ ],
369
+ },
370
+
371
+ // ── Zekrom (#644) — "The Bolt of Ideals" ────────────────
372
+ {
373
+ pokemonId: 644,
374
+ name: "The Bolt of Ideals",
375
+ steps: [
376
+ {
377
+ description: "Make 200 commits",
378
+ condition: { type: "counter", counter: "commits", threshold: 200 },
379
+ },
380
+ {
381
+ description: "Build successfully 100 times",
382
+ condition: { type: "counter", counter: "builds_succeeded", threshold: 100 },
383
+ },
384
+ {
385
+ description: "Catch 80 unique Pokemon",
386
+ condition: { type: "pokedex", minCaught: 80 },
387
+ },
388
+ {
389
+ description: "Code for 60 days (weekends off OK)",
390
+ condition: { type: "streak", minDays: 60 },
391
+ },
392
+ ],
393
+ },
394
+
395
+ // ── Kyurem (#646) — "The Frozen Merger" ─────────────────
396
+ {
397
+ pokemonId: 646,
398
+ name: "The Frozen Merger",
399
+ steps: [
400
+ {
401
+ description: "Catch both Reshiram and Zekrom",
402
+ condition: { type: "pokedex", minCaught: 90 },
403
+ },
404
+ {
405
+ description: "Earn the Lunar Badge",
406
+ condition: { type: "badge", badge: "lunar" },
407
+ },
408
+ {
409
+ description: "Reach level 50 on any Pokemon",
410
+ condition: { type: "level", minLevel: 50 },
411
+ },
412
+ {
413
+ description: "Code for 120 days (weekends off OK)",
414
+ condition: { type: "streak", minDays: 120 },
415
+ },
416
+ ],
417
+ },
418
+
419
+ // ═══════════════════════════════════════════════════════════
420
+ // GEN 6 — Kalos Legends
421
+ // ═══════════════════════════════════════════════════════════
422
+
423
+ // ── Xerneas (#716) — "The Tree of Life" ─────────────────
424
+ {
425
+ pokemonId: 716,
426
+ name: "The Tree of Life",
427
+ steps: [
428
+ {
429
+ description: "Create 100 files",
430
+ condition: { type: "counter", counter: "files_created", threshold: 100 },
431
+ },
432
+ {
433
+ description: "Earn the Growth Badge",
434
+ condition: { type: "badge", badge: "growth" },
435
+ },
436
+ {
437
+ description: "Catch 70 unique Pokemon",
438
+ condition: { type: "pokedex", minCaught: 70 },
439
+ },
440
+ {
441
+ description: "Code for 75 days (weekends off OK)",
442
+ condition: { type: "streak", minDays: 75 },
443
+ },
444
+ ],
445
+ },
446
+
447
+ // ── Yveltal (#717) — "The Destruction Debugger" ─────────
448
+ {
449
+ pokemonId: 717,
450
+ name: "The Destruction Debugger",
451
+ steps: [
452
+ {
453
+ description: "Fix 100 bugs",
454
+ condition: { type: "counter", counter: "bugs_fixed", threshold: 100 },
455
+ },
456
+ {
457
+ description: "Pass 400 tests",
458
+ condition: { type: "counter", counter: "tests_passed", threshold: 400 },
459
+ },
460
+ {
461
+ description: "Make 250 commits",
462
+ condition: { type: "counter", counter: "commits", threshold: 250 },
463
+ },
464
+ {
465
+ description: "Code for 75 days (weekends off OK)",
466
+ condition: { type: "streak", minDays: 75 },
467
+ },
468
+ ],
469
+ },
470
+
471
+ // ═══════════════════════════════════════════════════════════
472
+ // GEN 7 — Alola Legends
473
+ // ═══════════════════════════════════════════════════════════
474
+
475
+ // ── Solgaleo (#791) — "The Sunlit Deployer" ─────────────
476
+ {
477
+ pokemonId: 791,
478
+ name: "The Sunlit Deployer",
479
+ steps: [
480
+ {
481
+ description: "Build successfully 100 times",
482
+ condition: { type: "counter", counter: "builds_succeeded", threshold: 100 },
483
+ },
484
+ {
485
+ description: "Make 300 commits",
486
+ condition: { type: "counter", counter: "commits", threshold: 300 },
487
+ },
488
+ {
489
+ description: "Catch 100 unique Pokemon",
490
+ condition: { type: "pokedex", minCaught: 100 },
130
491
  },
131
492
  {
132
- description: "Code for 365 days (weekends off OK)",
133
- condition: { type: "streak", minDays: 365 },
493
+ description: "Code for 90 days (weekends off OK)",
494
+ condition: { type: "streak", minDays: 90 },
495
+ },
496
+ ],
497
+ },
498
+
499
+ // ── Lunala (#792) — "The Moonlit Reviewer" ──────────────
500
+ {
501
+ pokemonId: 792,
502
+ name: "The Moonlit Reviewer",
503
+ steps: [
504
+ {
505
+ description: "Perform 1000 searches",
506
+ condition: { type: "counter", counter: "searches", threshold: 1000 },
507
+ },
508
+ {
509
+ description: "Write 50 test files",
510
+ condition: { type: "counter", counter: "tests_written", threshold: 50 },
511
+ },
512
+ {
513
+ description: "Reach level 50 on any Pokemon",
514
+ condition: { type: "level", minLevel: 50 },
515
+ },
516
+ {
517
+ description: "Code for 90 days (weekends off OK)",
518
+ condition: { type: "streak", minDays: 90 },
519
+ },
520
+ ],
521
+ },
522
+
523
+ // ═══════════════════════════════════════════════════════════
524
+ // GEN 8 — Galar Legends
525
+ // ═══════════════════════════════════════════════════════════
526
+
527
+ // ── Zacian (#888) — "The Crowned Shipper" ───────────────
528
+ {
529
+ pokemonId: 888,
530
+ name: "The Crowned Shipper",
531
+ steps: [
532
+ {
533
+ description: "Make 250 commits",
534
+ condition: { type: "counter", counter: "commits", threshold: 250 },
535
+ },
536
+ {
537
+ description: "Build successfully 150 times",
538
+ condition: { type: "counter", counter: "builds_succeeded", threshold: 150 },
539
+ },
540
+ {
541
+ description: "Catch 120 unique Pokemon",
542
+ condition: { type: "pokedex", minCaught: 120 },
543
+ },
544
+ {
545
+ description: "Code for 120 days (weekends off OK)",
546
+ condition: { type: "streak", minDays: 120 },
547
+ },
548
+ ],
549
+ },
550
+
551
+ // ── Zamazenta (#889) — "The Shield of Stability" ────────
552
+ {
553
+ pokemonId: 889,
554
+ name: "The Shield of Stability",
555
+ steps: [
556
+ {
557
+ description: "Pass 500 tests",
558
+ condition: { type: "counter", counter: "tests_passed", threshold: 500 },
559
+ },
560
+ {
561
+ description: "Fix 200 bugs",
562
+ condition: { type: "counter", counter: "bugs_fixed", threshold: 200 },
563
+ },
564
+ {
565
+ description: "Edit 500 files",
566
+ condition: { type: "counter", counter: "files_edited", threshold: 500 },
567
+ },
568
+ {
569
+ description: "Code for 120 days (weekends off OK)",
570
+ condition: { type: "streak", minDays: 120 },
134
571
  },
135
572
  ],
136
573
  },
@@ -138,45 +575,12 @@ export const LEGENDARY_QUESTS: readonly LegendaryQuest[] = [
138
575
 
139
576
  // ── Quest Progress ───────────────────────────────────────────
140
577
 
141
- /** IDs of Water and Ice type Pokemon in Gen 1 Pokedex. */
142
- const WATER_ICE_POKEMON_IDS: ReadonlySet<number> = new Set([
143
- // Water types
144
- 7,
145
- 8,
146
- 9, // Squirtle line
147
- 54,
148
- 55, // Psyduck, Golduck
149
- 60,
150
- 61,
151
- 62, // Poliwag line
152
- 72,
153
- 73, // Tentacool, Tentacruel
154
- 79,
155
- 80, // Slowpoke, Slowbro
156
- 86,
157
- 87, // Seel, Dewgong (also Ice)
158
- 90,
159
- 91, // Shellder, Cloyster (also Ice)
160
- 98,
161
- 99, // Krabby, Kingler
162
- 116,
163
- 117, // Horsea, Seadra
164
- 118,
165
- 119, // Goldeen, Seaking
166
- 120,
167
- 121, // Staryu, Starmie
168
- 129,
169
- 130, // Magikarp, Gyarados
170
- 131, // Lapras (Water/Ice)
171
- 134, // Vaporeon
172
- 138,
173
- 139, // Omanyte, Omastar
174
- 140,
175
- 141, // Kabuto, Kabutops
176
- // Ice types (not already listed)
177
- 124, // Jynx (Ice/Psychic)
178
- 144, // Articuno (Ice/Flying)
179
- ]);
578
+ /** IDs of Water and Ice type Pokemon (all gens, not just Gen 1). */
579
+ function isWaterOrIceType(pokemonId: number): boolean {
580
+ const pokemon = POKEMON_BY_ID.get(pokemonId);
581
+ if (!pokemon) return false;
582
+ return pokemon.types.some((t) => t === "Water" || t === "Ice");
583
+ }
180
584
 
181
585
  /** Legendary bird Pokedex IDs. */
182
586
  const LEGENDARY_BIRD_IDS: readonly number[] = [144, 145, 146];
@@ -184,6 +588,18 @@ const LEGENDARY_BIRD_IDS: readonly number[] = [144, 145, 146];
184
588
  /** Mewtwo Pokedex ID. */
185
589
  const MEWTWO_ID = 150;
186
590
 
591
+ /** Kyogre and Groudon IDs. */
592
+ const KYOGRE_ID = 382;
593
+ const GROUDON_ID = 383;
594
+
595
+ /** Dialga and Palkia IDs. */
596
+ const DIALGA_ID = 483;
597
+ const PALKIA_ID = 484;
598
+
599
+ /** Reshiram and Zekrom IDs. */
600
+ const RESHIRAM_ID = 643;
601
+ const ZEKROM_ID = 644;
602
+
187
603
  export interface QuestProgress {
188
604
  readonly quest: LegendaryQuest;
189
605
  readonly stepsCompleted: number;
@@ -203,7 +619,7 @@ export function getQuestProgress(state: PlayerState): QuestProgress[] {
203
619
  if (isStepComplete(quest.pokemonId, step.description, step.condition, state)) {
204
620
  stepsCompleted++;
205
621
  } else {
206
- break; // Steps are sequential — stop at first incomplete.
622
+ break;
207
623
  }
208
624
  }
209
625
 
@@ -217,11 +633,6 @@ export function getQuestProgress(state: PlayerState): QuestProgress[] {
217
633
 
218
634
  // ── Internal Helpers ─────────────────────────────────────────
219
635
 
220
- /**
221
- * Check whether a quest step is complete.
222
- * Uses isConditionMet for standard conditions, with overrides
223
- * for steps that require specific-Pokemon or type-based checks.
224
- */
225
636
  function isStepComplete(
226
637
  questPokemonId: number,
227
638
  stepDescription: string,
@@ -230,7 +641,7 @@ function isStepComplete(
230
641
  ): boolean {
231
642
  // Articuno step 2: catch 3 Water/Ice type Pokemon
232
643
  if (questPokemonId === 144 && stepDescription.includes("Water or Ice")) {
233
- return countCaughtByIds(state, WATER_ICE_POKEMON_IDS) >= 3;
644
+ return countCaughtByType(state, isWaterOrIceType) >= 3;
234
645
  }
235
646
 
236
647
  // Mew step 2: catch all 3 legendary birds
@@ -243,21 +654,34 @@ function isStepComplete(
243
654
  return isPokemonCaught(state, MEWTWO_ID);
244
655
  }
245
656
 
657
+ // Rayquaza step 1: catch both Kyogre and Groudon
658
+ if (questPokemonId === 384 && stepDescription.includes("Kyogre and Groudon")) {
659
+ return isPokemonCaught(state, KYOGRE_ID) && isPokemonCaught(state, GROUDON_ID);
660
+ }
661
+
662
+ // Giratina step 1: catch both Dialga and Palkia
663
+ if (questPokemonId === 487 && stepDescription.includes("Dialga and Palkia")) {
664
+ return isPokemonCaught(state, DIALGA_ID) && isPokemonCaught(state, PALKIA_ID);
665
+ }
666
+
667
+ // Kyurem step 1: catch both Reshiram and Zekrom
668
+ if (questPokemonId === 646 && stepDescription.includes("Reshiram and Zekrom")) {
669
+ return isPokemonCaught(state, RESHIRAM_ID) && isPokemonCaught(state, ZEKROM_ID);
670
+ }
671
+
246
672
  // Default: delegate to the standard condition checker.
247
673
  return isConditionMet(condition, state);
248
674
  }
249
675
 
250
- /** Check whether a specific Pokemon ID has been caught in the Pokedex. */
251
676
  function isPokemonCaught(state: PlayerState, pokemonId: number): boolean {
252
677
  const entry = state.pokedex.entries[pokemonId];
253
678
  return entry !== undefined && entry.caught;
254
679
  }
255
680
 
256
- /** Count how many Pokemon from a given ID set have been caught. */
257
- function countCaughtByIds(state: PlayerState, ids: ReadonlySet<number>): number {
681
+ function countCaughtByType(state: PlayerState, typePredicate: (id: number) => boolean): number {
258
682
  let count = 0;
259
- for (const id of ids) {
260
- if (isPokemonCaught(state, id)) {
683
+ for (const [idStr, entry] of Object.entries(state.pokedex.entries)) {
684
+ if (entry.caught && typePredicate(Number(idStr))) {
261
685
  count++;
262
686
  }
263
687
  }