@umang-boss/claudemon 1.0.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.
Files changed (203) hide show
  1. package/README.md +164 -0
  2. package/bin/claudemon.js +52 -0
  3. package/bunfig.toml +2 -0
  4. package/cli/doctor.ts +334 -0
  5. package/cli/index.ts +42 -0
  6. package/cli/install.ts +248 -0
  7. package/cli/shared.ts +102 -0
  8. package/cli/uninstall.ts +155 -0
  9. package/cli/update.ts +318 -0
  10. package/hooks/post-tool-use.sh +127 -0
  11. package/hooks/stop.sh +49 -0
  12. package/hooks/user-prompt-submit.sh +73 -0
  13. package/package.json +68 -0
  14. package/scripts/download-colorscripts.ts +311 -0
  15. package/skills/buddy/SKILL.md +47 -0
  16. package/sprites/colorscripts/small/1-bulbasaur.txt +11 -0
  17. package/sprites/colorscripts/small/10-caterpie.txt +9 -0
  18. package/sprites/colorscripts/small/100-voltorb.txt +8 -0
  19. package/sprites/colorscripts/small/101-electrode.txt +9 -0
  20. package/sprites/colorscripts/small/102-exeggcute.txt +10 -0
  21. package/sprites/colorscripts/small/103-exeggutor.txt +23 -0
  22. package/sprites/colorscripts/small/104-cubone.txt +11 -0
  23. package/sprites/colorscripts/small/105-marowak.txt +16 -0
  24. package/sprites/colorscripts/small/106-hitmonlee.txt +16 -0
  25. package/sprites/colorscripts/small/107-hitmonchan.txt +19 -0
  26. package/sprites/colorscripts/small/108-lickitung.txt +10 -0
  27. package/sprites/colorscripts/small/109-koffing.txt +14 -0
  28. package/sprites/colorscripts/small/11-metapod.txt +10 -0
  29. package/sprites/colorscripts/small/110-weezing.txt +23 -0
  30. package/sprites/colorscripts/small/111-rhyhorn.txt +11 -0
  31. package/sprites/colorscripts/small/112-rhydon.txt +20 -0
  32. package/sprites/colorscripts/small/113-chansey.txt +11 -0
  33. package/sprites/colorscripts/small/114-tangela.txt +10 -0
  34. package/sprites/colorscripts/small/115-kangaskhan.txt +18 -0
  35. package/sprites/colorscripts/small/116-horsea.txt +10 -0
  36. package/sprites/colorscripts/small/117-seadra.txt +11 -0
  37. package/sprites/colorscripts/small/118-goldeen.txt +11 -0
  38. package/sprites/colorscripts/small/119-seaking.txt +16 -0
  39. package/sprites/colorscripts/small/12-butterfree.txt +20 -0
  40. package/sprites/colorscripts/small/120-staryu.txt +10 -0
  41. package/sprites/colorscripts/small/121-starmie.txt +17 -0
  42. package/sprites/colorscripts/small/122-mr-mime.txt +18 -0
  43. package/sprites/colorscripts/small/123-scyther.txt +21 -0
  44. package/sprites/colorscripts/small/124-jynx.txt +18 -0
  45. package/sprites/colorscripts/small/125-electabuzz.txt +19 -0
  46. package/sprites/colorscripts/small/126-magmar.txt +19 -0
  47. package/sprites/colorscripts/small/127-pinsir.txt +19 -0
  48. package/sprites/colorscripts/small/128-tauros.txt +20 -0
  49. package/sprites/colorscripts/small/129-magikarp.txt +13 -0
  50. package/sprites/colorscripts/small/13-weedle.txt +10 -0
  51. package/sprites/colorscripts/small/130-gyarados.txt +21 -0
  52. package/sprites/colorscripts/small/131-lapras.txt +19 -0
  53. package/sprites/colorscripts/small/132-ditto.txt +8 -0
  54. package/sprites/colorscripts/small/133-eevee.txt +10 -0
  55. package/sprites/colorscripts/small/134-vaporeon.txt +16 -0
  56. package/sprites/colorscripts/small/135-jolteon.txt +17 -0
  57. package/sprites/colorscripts/small/136-flareon.txt +18 -0
  58. package/sprites/colorscripts/small/137-porygon.txt +10 -0
  59. package/sprites/colorscripts/small/138-omanyte.txt +10 -0
  60. package/sprites/colorscripts/small/139-omastar.txt +18 -0
  61. package/sprites/colorscripts/small/14-kakuna.txt +10 -0
  62. package/sprites/colorscripts/small/140-kabuto.txt +8 -0
  63. package/sprites/colorscripts/small/141-kabutops.txt +17 -0
  64. package/sprites/colorscripts/small/142-aerodactyl.txt +17 -0
  65. package/sprites/colorscripts/small/143-snorlax.txt +21 -0
  66. package/sprites/colorscripts/small/144-articuno.txt +24 -0
  67. package/sprites/colorscripts/small/145-zapdos.txt +20 -0
  68. package/sprites/colorscripts/small/146-moltres.txt +23 -0
  69. package/sprites/colorscripts/small/147-dratini.txt +10 -0
  70. package/sprites/colorscripts/small/148-dragonair.txt +12 -0
  71. package/sprites/colorscripts/small/149-dragonite.txt +21 -0
  72. package/sprites/colorscripts/small/15-beedrill.txt +13 -0
  73. package/sprites/colorscripts/small/150-mewtwo.txt +22 -0
  74. package/sprites/colorscripts/small/151-mew.txt +14 -0
  75. package/sprites/colorscripts/small/16-pidgey.txt +10 -0
  76. package/sprites/colorscripts/small/17-pidgeotto.txt +11 -0
  77. package/sprites/colorscripts/small/18-pidgeot.txt +18 -0
  78. package/sprites/colorscripts/small/19-rattata.txt +12 -0
  79. package/sprites/colorscripts/small/2-ivysaur.txt +11 -0
  80. package/sprites/colorscripts/small/20-raticate.txt +12 -0
  81. package/sprites/colorscripts/small/21-spearow.txt +9 -0
  82. package/sprites/colorscripts/small/22-fearow.txt +12 -0
  83. package/sprites/colorscripts/small/23-ekans.txt +12 -0
  84. package/sprites/colorscripts/small/24-arbok.txt +16 -0
  85. package/sprites/colorscripts/small/25-pikachu.txt +11 -0
  86. package/sprites/colorscripts/small/26-raichu.txt +19 -0
  87. package/sprites/colorscripts/small/27-sandshrew.txt +10 -0
  88. package/sprites/colorscripts/small/28-sandslash.txt +16 -0
  89. package/sprites/colorscripts/small/29-nidoran-f.txt +11 -0
  90. package/sprites/colorscripts/small/3-venusaur.txt +21 -0
  91. package/sprites/colorscripts/small/30-nidorina.txt +12 -0
  92. package/sprites/colorscripts/small/31-nidoqueen.txt +19 -0
  93. package/sprites/colorscripts/small/32-nidoran-m.txt +11 -0
  94. package/sprites/colorscripts/small/33-nidorino.txt +12 -0
  95. package/sprites/colorscripts/small/34-nidoking.txt +18 -0
  96. package/sprites/colorscripts/small/35-clefairy.txt +11 -0
  97. package/sprites/colorscripts/small/36-clefable.txt +17 -0
  98. package/sprites/colorscripts/small/37-vulpix.txt +11 -0
  99. package/sprites/colorscripts/small/38-ninetales.txt +18 -0
  100. package/sprites/colorscripts/small/39-jigglypuff.txt +11 -0
  101. package/sprites/colorscripts/small/4-charmander.txt +11 -0
  102. package/sprites/colorscripts/small/40-wigglytuff.txt +20 -0
  103. package/sprites/colorscripts/small/41-zubat.txt +11 -0
  104. package/sprites/colorscripts/small/42-golbat.txt +18 -0
  105. package/sprites/colorscripts/small/43-oddish.txt +11 -0
  106. package/sprites/colorscripts/small/44-gloom.txt +12 -0
  107. package/sprites/colorscripts/small/45-vileplume.txt +17 -0
  108. package/sprites/colorscripts/small/46-paras.txt +11 -0
  109. package/sprites/colorscripts/small/47-parasect.txt +12 -0
  110. package/sprites/colorscripts/small/48-venonat.txt +14 -0
  111. package/sprites/colorscripts/small/49-venomoth.txt +19 -0
  112. package/sprites/colorscripts/small/5-charmeleon.txt +13 -0
  113. package/sprites/colorscripts/small/50-diglett.txt +8 -0
  114. package/sprites/colorscripts/small/51-dugtrio.txt +18 -0
  115. package/sprites/colorscripts/small/52-meowth.txt +12 -0
  116. package/sprites/colorscripts/small/53-persian.txt +20 -0
  117. package/sprites/colorscripts/small/54-psyduck.txt +12 -0
  118. package/sprites/colorscripts/small/55-golduck.txt +17 -0
  119. package/sprites/colorscripts/small/56-mankey.txt +11 -0
  120. package/sprites/colorscripts/small/57-primeape.txt +13 -0
  121. package/sprites/colorscripts/small/58-growlithe.txt +12 -0
  122. package/sprites/colorscripts/small/59-arcanine.txt +20 -0
  123. package/sprites/colorscripts/small/6-charizard.txt +21 -0
  124. package/sprites/colorscripts/small/60-poliwag.txt +9 -0
  125. package/sprites/colorscripts/small/61-poliwhirl.txt +11 -0
  126. package/sprites/colorscripts/small/62-poliwrath.txt +17 -0
  127. package/sprites/colorscripts/small/63-abra.txt +12 -0
  128. package/sprites/colorscripts/small/64-kadabra.txt +14 -0
  129. package/sprites/colorscripts/small/65-alakazam.txt +19 -0
  130. package/sprites/colorscripts/small/66-machop.txt +11 -0
  131. package/sprites/colorscripts/small/67-machoke.txt +12 -0
  132. package/sprites/colorscripts/small/68-machamp.txt +19 -0
  133. package/sprites/colorscripts/small/69-bellsprout.txt +9 -0
  134. package/sprites/colorscripts/small/7-squirtle.txt +10 -0
  135. package/sprites/colorscripts/small/70-weepinbell.txt +11 -0
  136. package/sprites/colorscripts/small/71-victreebel.txt +17 -0
  137. package/sprites/colorscripts/small/72-tentacool.txt +12 -0
  138. package/sprites/colorscripts/small/73-tentacruel.txt +20 -0
  139. package/sprites/colorscripts/small/74-geodude.txt +9 -0
  140. package/sprites/colorscripts/small/75-graveler.txt +12 -0
  141. package/sprites/colorscripts/small/76-golem.txt +18 -0
  142. package/sprites/colorscripts/small/77-ponyta.txt +13 -0
  143. package/sprites/colorscripts/small/78-rapidash.txt +18 -0
  144. package/sprites/colorscripts/small/79-slowpoke.txt +12 -0
  145. package/sprites/colorscripts/small/8-wartortle.txt +12 -0
  146. package/sprites/colorscripts/small/80-slowbro.txt +18 -0
  147. package/sprites/colorscripts/small/81-magnemite.txt +9 -0
  148. package/sprites/colorscripts/small/82-magneton.txt +18 -0
  149. package/sprites/colorscripts/small/83-farfetchd.txt +12 -0
  150. package/sprites/colorscripts/small/84-doduo.txt +10 -0
  151. package/sprites/colorscripts/small/85-dodrio.txt +17 -0
  152. package/sprites/colorscripts/small/86-seel.txt +13 -0
  153. package/sprites/colorscripts/small/87-dewgong.txt +20 -0
  154. package/sprites/colorscripts/small/88-grimer.txt +10 -0
  155. package/sprites/colorscripts/small/89-muk.txt +14 -0
  156. package/sprites/colorscripts/small/9-blastoise.txt +20 -0
  157. package/sprites/colorscripts/small/90-shellder.txt +10 -0
  158. package/sprites/colorscripts/small/91-cloyster.txt +18 -0
  159. package/sprites/colorscripts/small/92-gastly.txt +12 -0
  160. package/sprites/colorscripts/small/93-haunter.txt +14 -0
  161. package/sprites/colorscripts/small/94-gengar.txt +19 -0
  162. package/sprites/colorscripts/small/95-onix.txt +22 -0
  163. package/sprites/colorscripts/small/96-drowzee.txt +12 -0
  164. package/sprites/colorscripts/small/97-hypno.txt +19 -0
  165. package/sprites/colorscripts/small/98-krabby.txt +12 -0
  166. package/sprites/colorscripts/small/99-kingler.txt +20 -0
  167. package/src/engine/constants.ts +121 -0
  168. package/src/engine/encounter-pool.ts +71 -0
  169. package/src/engine/encounters.ts +308 -0
  170. package/src/engine/evolution-data.ts +535 -0
  171. package/src/engine/evolution.ts +310 -0
  172. package/src/engine/pokemon-data.ts +1838 -0
  173. package/src/engine/reactions.ts +877 -0
  174. package/src/engine/starter-pool.ts +47 -0
  175. package/src/engine/stats.ts +97 -0
  176. package/src/engine/types.ts +312 -0
  177. package/src/engine/xp.ts +135 -0
  178. package/src/gamification/achievements.ts +204 -0
  179. package/src/gamification/legendary-quests.ts +265 -0
  180. package/src/gamification/milestones.ts +86 -0
  181. package/src/hooks/award-xp.ts +131 -0
  182. package/src/hooks/increment-counter.ts +27 -0
  183. package/src/server/index.ts +78 -0
  184. package/src/server/instructions.ts +194 -0
  185. package/src/server/tools/achievements.ts +118 -0
  186. package/src/server/tools/catch.ts +295 -0
  187. package/src/server/tools/display-helpers.ts +35 -0
  188. package/src/server/tools/evolve.ts +236 -0
  189. package/src/server/tools/legendary.ts +78 -0
  190. package/src/server/tools/party.ts +251 -0
  191. package/src/server/tools/pet.ts +124 -0
  192. package/src/server/tools/pokedex.ts +286 -0
  193. package/src/server/tools/rename.ts +63 -0
  194. package/src/server/tools/show.ts +136 -0
  195. package/src/server/tools/starter.ts +175 -0
  196. package/src/server/tools/stats.ts +123 -0
  197. package/src/server/tools/visibility.ts +65 -0
  198. package/src/sprites/index.ts +45 -0
  199. package/src/state/io.ts +91 -0
  200. package/src/state/schemas.ts +131 -0
  201. package/src/state/state-manager.ts +321 -0
  202. package/statusline/buddy-status.sh +233 -0
  203. package/tsconfig.json +37 -0
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Achievement definitions and unlock-checking logic for Claudemon.
3
+ * All achievements are derived from PlayerState — pure functions, no side effects.
4
+ */
5
+
6
+ import type {
7
+ Achievement,
8
+ AchievementCondition,
9
+ PlayerState,
10
+ UnlockedAchievement,
11
+ } from "../engine/types.js";
12
+
13
+ // ── Achievement Definitions ──────────────────────────────────
14
+
15
+ export const ACHIEVEMENTS: readonly Achievement[] = [
16
+ // ── Trainer Milestones ───────────────────────────────────
17
+ {
18
+ id: "first_steps",
19
+ name: "First Steps",
20
+ description: "Pick your starter Pokemon",
21
+ category: "trainer",
22
+ condition: { type: "party_size", minSize: 1 },
23
+ },
24
+ {
25
+ id: "getting_started",
26
+ name: "Getting Started",
27
+ description: "Reach level 10",
28
+ category: "trainer",
29
+ condition: { type: "level", minLevel: 10 },
30
+ },
31
+ {
32
+ id: "seasoned",
33
+ name: "Seasoned Trainer",
34
+ description: "Reach level 50",
35
+ category: "trainer",
36
+ condition: { type: "level", minLevel: 50 },
37
+ },
38
+ {
39
+ id: "master",
40
+ name: "Pokemon Master",
41
+ description: "Reach level 100",
42
+ category: "trainer",
43
+ condition: { type: "level", minLevel: 100 },
44
+ },
45
+ {
46
+ id: "full_party",
47
+ name: "Full Party",
48
+ description: "Have 6 Pokemon in your party",
49
+ category: "trainer",
50
+ condition: { type: "party_size", minSize: 6 },
51
+ },
52
+ {
53
+ id: "collector_50",
54
+ name: "Collector",
55
+ description: "Catch 50 unique Pokemon",
56
+ category: "trainer",
57
+ condition: { type: "pokedex", minCaught: 50 },
58
+ },
59
+ {
60
+ id: "completionist",
61
+ name: "Completionist",
62
+ description: "Catch all 151 Pokemon",
63
+ category: "trainer",
64
+ condition: { type: "pokedex", minCaught: 151 },
65
+ },
66
+
67
+ // ── Coding Achievements ──────────────────────────────────
68
+ {
69
+ id: "bug_catcher_10",
70
+ name: "Bug Catcher",
71
+ description: "Fix 10 errors",
72
+ category: "coding",
73
+ condition: { type: "counter", counter: "bugs_fixed", threshold: 10 },
74
+ },
75
+ {
76
+ id: "exterminator",
77
+ name: "Exterminator",
78
+ description: "Fix 100 errors",
79
+ category: "coding",
80
+ condition: { type: "counter", counter: "bugs_fixed", threshold: 100 },
81
+ },
82
+ {
83
+ id: "test_ace",
84
+ name: "Test Ace",
85
+ description: "Pass 100 tests",
86
+ category: "coding",
87
+ condition: { type: "counter", counter: "tests_passed", threshold: 100 },
88
+ },
89
+ {
90
+ id: "ci_champion",
91
+ name: "CI Champion",
92
+ description: "50 successful builds",
93
+ category: "coding",
94
+ condition: { type: "counter", counter: "builds_succeeded", threshold: 50 },
95
+ },
96
+ {
97
+ id: "refactor_king",
98
+ name: "Refactor King",
99
+ description: "Complete 10 large refactors",
100
+ category: "coding",
101
+ condition: { type: "counter", counter: "large_refactors", threshold: 10 },
102
+ },
103
+ {
104
+ id: "iron_coder",
105
+ name: "Iron Coder",
106
+ description: "Maintain a 7-day coding streak",
107
+ category: "coding",
108
+ condition: { type: "streak", minDays: 7 },
109
+ },
110
+ {
111
+ id: "marathon",
112
+ name: "Marathon",
113
+ description: "Maintain a 30-day coding streak",
114
+ category: "coding",
115
+ condition: { type: "streak", minDays: 30 },
116
+ },
117
+ {
118
+ id: "centurion",
119
+ name: "Centurion",
120
+ description: "Maintain a 100-day coding streak",
121
+ category: "coding",
122
+ condition: { type: "streak", minDays: 100 },
123
+ },
124
+
125
+ // ── Pokemon Achievements ─────────────────────────────────
126
+ {
127
+ id: "evolution",
128
+ name: "Evolutionary",
129
+ description: "Evolve a Pokemon",
130
+ category: "pokemon",
131
+ condition: { type: "evolution" },
132
+ },
133
+ {
134
+ id: "badge_collector",
135
+ name: "Badge Collector",
136
+ description: "Earn your first badge",
137
+ category: "pokemon",
138
+ condition: { type: "badge", badge: "blaze" },
139
+ },
140
+ ];
141
+
142
+ // ── Condition Checker ────────────────────────────────────────
143
+
144
+ /** Check whether a single achievement condition is satisfied by the current player state. */
145
+ export function isConditionMet(condition: AchievementCondition, state: PlayerState): boolean {
146
+ switch (condition.type) {
147
+ case "counter":
148
+ return (state.counters[condition.counter] ?? 0) >= condition.threshold;
149
+
150
+ case "level": {
151
+ const highestLevel = getHighestPartyLevel(state);
152
+ return highestLevel >= condition.minLevel;
153
+ }
154
+
155
+ case "pokedex":
156
+ return state.pokedex.totalCaught >= condition.minCaught;
157
+
158
+ case "streak":
159
+ return state.streak.currentStreak >= condition.minDays;
160
+
161
+ case "badge":
162
+ return state.badges.includes(condition.badge);
163
+
164
+ case "evolution":
165
+ return hasAnyEvolvedPokemon(state);
166
+
167
+ case "party_size":
168
+ return state.party.length >= condition.minSize;
169
+ }
170
+ }
171
+
172
+ // ── Achievement Discovery ────────────────────────────────────
173
+
174
+ /** Return achievements that are newly unlocked (not already in state.achievements). */
175
+ export function checkNewAchievements(state: PlayerState): Achievement[] {
176
+ const alreadyUnlocked = new Set(state.achievements.map((a) => a.achievementId));
177
+
178
+ return ACHIEVEMENTS.filter(
179
+ (achievement) =>
180
+ !alreadyUnlocked.has(achievement.id) && isConditionMet(achievement.condition, state),
181
+ );
182
+ }
183
+
184
+ /** Create a timestamped unlock record for an achievement. */
185
+ export function unlockAchievement(achievementId: string): UnlockedAchievement {
186
+ return {
187
+ achievementId,
188
+ unlockedAt: new Date().toISOString(),
189
+ };
190
+ }
191
+
192
+ // ── Internal Helpers ─────────────────────────────────────────
193
+
194
+ /** Get the highest level among all party Pokemon (0 if party is empty). */
195
+ function getHighestPartyLevel(state: PlayerState): number {
196
+ if (state.party.length === 0) return 0;
197
+ return Math.max(...state.party.map((p) => p.level));
198
+ }
199
+
200
+ /** Check whether any Pokemon in party or PC box has evolved (evolvedAt is set). */
201
+ function hasAnyEvolvedPokemon(state: PlayerState): boolean {
202
+ const allPokemon = [...state.party, ...state.pcBox];
203
+ return allPokemon.some((p) => p.evolvedAt !== null);
204
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Legendary quest chain definitions for Claudemon.
3
+ * Each legendary Pokemon requires completing a multi-step quest
4
+ * tied to sustained coding activity.
5
+ */
6
+
7
+ import type { AchievementCondition, LegendaryQuest, PlayerState } from "../engine/types.js";
8
+ import { isConditionMet } from "./achievements.js";
9
+
10
+ // ── Quest Definitions ────────────────────────────────────────
11
+
12
+ export const LEGENDARY_QUESTS: readonly LegendaryQuest[] = [
13
+ // ── Articuno (#144) — "The Ice Bird of Endurance" ────────
14
+ {
15
+ pokemonId: 144,
16
+ name: "The Ice Bird of Endurance",
17
+ steps: [
18
+ {
19
+ description: "Maintain a 30-day coding streak",
20
+ condition: { type: "streak", minDays: 30 },
21
+ },
22
+ {
23
+ description: "Catch 3 Water or Ice type Pokemon",
24
+ // Approximated as pokedex gate; real type-check in getQuestProgress.
25
+ condition: { type: "pokedex", minCaught: 3 },
26
+ },
27
+ {
28
+ description: "Maintain a 100-day coding streak",
29
+ condition: { type: "streak", minDays: 100 },
30
+ },
31
+ {
32
+ description: "Reach level 50 on your active Pokemon",
33
+ condition: { type: "level", minLevel: 50 },
34
+ },
35
+ ],
36
+ },
37
+
38
+ // ── Zapdos (#145) — "The Thunder of Testing" ────────────
39
+ {
40
+ pokemonId: 145,
41
+ name: "The Thunder of Testing",
42
+ steps: [
43
+ {
44
+ description: "Pass 100 tests",
45
+ condition: { type: "counter", counter: "tests_passed", threshold: 100 },
46
+ },
47
+ {
48
+ description: "Write 50 test files",
49
+ condition: { type: "counter", counter: "tests_written", threshold: 50 },
50
+ },
51
+ {
52
+ description: "Pass 500 tests",
53
+ condition: { type: "counter", counter: "tests_passed", threshold: 500 },
54
+ },
55
+ {
56
+ description: "Pass 1000 tests lifetime",
57
+ condition: { type: "counter", counter: "tests_passed", threshold: 1000 },
58
+ },
59
+ ],
60
+ },
61
+
62
+ // ── Moltres (#146) — "The Flame of Debugging" ──────────
63
+ {
64
+ pokemonId: 146,
65
+ name: "The Flame of Debugging",
66
+ steps: [
67
+ {
68
+ description: "Fix 50 bugs",
69
+ condition: { type: "counter", counter: "bugs_fixed", threshold: 50 },
70
+ },
71
+ {
72
+ description: "Earn the Blaze Badge",
73
+ condition: { type: "badge", badge: "blaze" },
74
+ },
75
+ {
76
+ description: "Fix 200 bugs",
77
+ condition: { type: "counter", counter: "bugs_fixed", threshold: 200 },
78
+ },
79
+ {
80
+ description: "Fix 500 bugs lifetime",
81
+ condition: { type: "counter", counter: "bugs_fixed", threshold: 500 },
82
+ },
83
+ ],
84
+ },
85
+
86
+ // ── Mewtwo (#150) — "The Ultimate Creation" ─────────────
87
+ {
88
+ pokemonId: 150,
89
+ name: "The Ultimate Creation",
90
+ steps: [
91
+ {
92
+ description: "Catch 50 unique Pokemon",
93
+ condition: { type: "pokedex", minCaught: 50 },
94
+ },
95
+ {
96
+ description: "Reach level 75 on any Pokemon",
97
+ condition: { type: "level", minLevel: 75 },
98
+ },
99
+ {
100
+ description: "Catch 100 unique Pokemon",
101
+ condition: { type: "pokedex", minCaught: 100 },
102
+ },
103
+ {
104
+ description: "Catch 140 unique Pokemon",
105
+ condition: { type: "pokedex", minCaught: 140 },
106
+ },
107
+ ],
108
+ },
109
+
110
+ // ── Mew (#151) — "The Myth" ─────────────────────────────
111
+ {
112
+ pokemonId: 151,
113
+ name: "The Myth",
114
+ steps: [
115
+ {
116
+ description: "Maintain a 100-day coding streak",
117
+ condition: { type: "streak", minDays: 100 },
118
+ },
119
+ {
120
+ 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 },
124
+ },
125
+ {
126
+ 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
+ },
131
+ {
132
+ description: "Maintain a 365-day coding streak",
133
+ condition: { type: "streak", minDays: 365 },
134
+ },
135
+ ],
136
+ },
137
+ ];
138
+
139
+ // ── Quest Progress ───────────────────────────────────────────
140
+
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
+ ]);
180
+
181
+ /** Legendary bird Pokedex IDs. */
182
+ const LEGENDARY_BIRD_IDS: readonly number[] = [144, 145, 146];
183
+
184
+ /** Mewtwo Pokedex ID. */
185
+ const MEWTWO_ID = 150;
186
+
187
+ export interface QuestProgress {
188
+ readonly quest: LegendaryQuest;
189
+ readonly stepsCompleted: number;
190
+ readonly totalSteps: number;
191
+ }
192
+
193
+ /**
194
+ * Calculate completion progress for every legendary quest.
195
+ * Steps are checked sequentially — a later step cannot complete
196
+ * until all earlier steps are done.
197
+ */
198
+ export function getQuestProgress(state: PlayerState): QuestProgress[] {
199
+ return LEGENDARY_QUESTS.map((quest) => {
200
+ let stepsCompleted = 0;
201
+
202
+ for (const step of quest.steps) {
203
+ if (isStepComplete(quest.pokemonId, step.description, step.condition, state)) {
204
+ stepsCompleted++;
205
+ } else {
206
+ break; // Steps are sequential — stop at first incomplete.
207
+ }
208
+ }
209
+
210
+ return {
211
+ quest,
212
+ stepsCompleted,
213
+ totalSteps: quest.steps.length,
214
+ };
215
+ });
216
+ }
217
+
218
+ // ── Internal Helpers ─────────────────────────────────────────
219
+
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
+ function isStepComplete(
226
+ questPokemonId: number,
227
+ stepDescription: string,
228
+ condition: AchievementCondition,
229
+ state: PlayerState,
230
+ ): boolean {
231
+ // Articuno step 2: catch 3 Water/Ice type Pokemon
232
+ if (questPokemonId === 144 && stepDescription.includes("Water or Ice")) {
233
+ return countCaughtByIds(state, WATER_ICE_POKEMON_IDS) >= 3;
234
+ }
235
+
236
+ // Mew step 2: catch all 3 legendary birds
237
+ if (questPokemonId === 151 && stepDescription.includes("legendary birds")) {
238
+ return LEGENDARY_BIRD_IDS.every((id) => isPokemonCaught(state, id));
239
+ }
240
+
241
+ // Mew step 3: catch Mewtwo
242
+ if (questPokemonId === 151 && stepDescription.includes("Mewtwo")) {
243
+ return isPokemonCaught(state, MEWTWO_ID);
244
+ }
245
+
246
+ // Default: delegate to the standard condition checker.
247
+ return isConditionMet(condition, state);
248
+ }
249
+
250
+ /** Check whether a specific Pokemon ID has been caught in the Pokedex. */
251
+ function isPokemonCaught(state: PlayerState, pokemonId: number): boolean {
252
+ const entry = state.pokedex.entries[pokemonId];
253
+ return entry !== undefined && entry.caught;
254
+ }
255
+
256
+ /** Count how many Pokemon from a given ID set have been caught. */
257
+ function countCaughtByIds(state: PlayerState, ids: ReadonlySet<number>): number {
258
+ let count = 0;
259
+ for (const id of ids) {
260
+ if (isPokemonCaught(state, id)) {
261
+ count++;
262
+ }
263
+ }
264
+ return count;
265
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Milestone discovery definitions for Claudemon.
3
+ * Specific Pokemon appear when coding counters reach defined thresholds.
4
+ * Check these after every counter increment.
5
+ */
6
+
7
+ import type { EventCounterKey } from "../engine/types.js";
8
+
9
+ // ── Milestone Types ──────────────────────────────────────────
10
+
11
+ export interface MilestoneDiscovery {
12
+ readonly pokemonId: number;
13
+ readonly counter: EventCounterKey;
14
+ readonly threshold: number;
15
+ readonly message: string;
16
+ }
17
+
18
+ // ── Milestone Definitions ────────────────────────────────────
19
+
20
+ export const MILESTONES: readonly MilestoneDiscovery[] = [
21
+ {
22
+ pokemonId: 16, // Pidgey
23
+ counter: "commits",
24
+ threshold: 1,
25
+ message: "Every journey starts with a first step!",
26
+ },
27
+ {
28
+ pokemonId: 66, // Machop
29
+ counter: "tests_passed",
30
+ threshold: 10,
31
+ message: "Your code is getting stronger!",
32
+ },
33
+ {
34
+ pokemonId: 74, // Geodude
35
+ counter: "builds_succeeded",
36
+ threshold: 1,
37
+ message: "Solid foundations!",
38
+ },
39
+ {
40
+ pokemonId: 63, // Abra
41
+ counter: "commits",
42
+ threshold: 50,
43
+ message: "You're getting quick!",
44
+ },
45
+ {
46
+ pokemonId: 89, // Muk
47
+ counter: "bugs_fixed",
48
+ threshold: 100,
49
+ message: "You've seen some ugly code...",
50
+ },
51
+ {
52
+ pokemonId: 129, // Magikarp
53
+ counter: "commits",
54
+ threshold: 200,
55
+ message: "Humble beginnings... just wait",
56
+ },
57
+ {
58
+ pokemonId: 137, // Porygon
59
+ counter: "tests_written",
60
+ threshold: 100,
61
+ message: "A digital creation for a digital creator!",
62
+ },
63
+ {
64
+ pokemonId: 143, // Snorlax
65
+ counter: "files_edited",
66
+ threshold: 500,
67
+ message: "All that hard work... time for a nap?",
68
+ },
69
+ ];
70
+
71
+ // ── Milestone Checker ────────────────────────────────────────
72
+
73
+ /**
74
+ * Return milestones whose thresholds are met but whose Pokemon
75
+ * have not yet been caught (not in the provided pokedex set).
76
+ */
77
+ export function checkNewMilestones(
78
+ counters: Record<string, number>,
79
+ caughtPokemonIds: Set<number>,
80
+ ): MilestoneDiscovery[] {
81
+ return MILESTONES.filter(
82
+ (milestone) =>
83
+ !caughtPokemonIds.has(milestone.pokemonId) &&
84
+ (counters[milestone.counter] ?? 0) >= milestone.threshold,
85
+ );
86
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * award-xp.ts — Lightweight bun script called by the PostToolUse hook.
3
+ * Usage: bun run award-xp.ts <event_type> <counter_key>
4
+ *
5
+ * Loads state, awards XP to the active Pokemon, applies stat boosts,
6
+ * increments the event counter, updates the streak, and saves.
7
+ * Emits a terminal bell on level-up.
8
+ */
9
+
10
+ import type { XpEventType, EventCounterKey } from "../engine/types.js";
11
+ import { BELL } from "../engine/constants.js";
12
+ import { StateManager } from "../state/state-manager.js";
13
+ import { addXp, createXpEvent } from "../engine/xp.js";
14
+ import { applyStatBoost } from "../engine/stats.js";
15
+ import { POKEMON_BY_ID } from "../engine/pokemon-data.js";
16
+ import { checkEvolution, getNewlyEarnedBadges } from "../engine/evolution.js";
17
+ import { shouldTriggerEncounter, generateEncounter } from "../engine/encounters.js";
18
+
19
+ const eventType = process.argv[2] as XpEventType;
20
+ const counterKey = process.argv[3] as EventCounterKey | undefined;
21
+
22
+ if (!eventType) {
23
+ process.exit(0);
24
+ }
25
+
26
+ const stateManager = StateManager.getInstance();
27
+ const state = await stateManager.load();
28
+
29
+ // No state file = first run, nothing to do yet
30
+ if (!state) {
31
+ process.exit(0);
32
+ }
33
+
34
+ const pokemon = stateManager.getActivePokemon();
35
+ if (!pokemon) {
36
+ process.exit(0);
37
+ }
38
+
39
+ const pokemonData = POKEMON_BY_ID.get(pokemon.pokemonId);
40
+ if (!pokemonData) {
41
+ process.exit(0);
42
+ }
43
+
44
+ // Create XP event from the event type
45
+ const xpEvent = createXpEvent(eventType);
46
+
47
+ // Award XP and check for level-up
48
+ const levelUp = addXp(pokemon, xpEvent.xp, pokemonData);
49
+
50
+ // Apply stat boost if the event provides one
51
+ if (xpEvent.statBoost && xpEvent.boostAmount > 0) {
52
+ applyStatBoost(pokemon, xpEvent.statBoost, xpEvent.boostAmount);
53
+ }
54
+
55
+ // Track total XP earned by the trainer
56
+ state.totalXpEarned += xpEvent.xp;
57
+
58
+ // Increment the event counter directly on state (avoids extra save from incrementCounter)
59
+ if (counterKey) {
60
+ state.counters[counterKey] += 1;
61
+ }
62
+
63
+ // Update the daily coding streak
64
+ // Inline the streak logic to avoid the extra save from updateStreak()
65
+ const today = new Date();
66
+ const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
67
+ const { streak } = state;
68
+
69
+ if (streak.lastActiveDate !== todayStr) {
70
+ if (streak.lastActiveDate === null) {
71
+ streak.currentStreak = 1;
72
+ streak.totalDaysActive = 1;
73
+ } else {
74
+ const last = new Date(streak.lastActiveDate);
75
+ const now = new Date(todayStr);
76
+ const diffMs = now.getTime() - last.getTime();
77
+ const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
78
+
79
+ if (diffDays === 1) {
80
+ streak.currentStreak += 1;
81
+ } else {
82
+ streak.currentStreak = 1;
83
+ }
84
+ streak.totalDaysActive += 1;
85
+ }
86
+
87
+ if (streak.currentStreak > streak.longestStreak) {
88
+ streak.longestStreak = streak.currentStreak;
89
+ }
90
+
91
+ streak.lastActiveDate = todayStr;
92
+ }
93
+
94
+ // Check for newly earned badges
95
+ const newBadges = getNewlyEarnedBadges(state);
96
+ for (const badge of newBadges) {
97
+ state.badges.push(badge);
98
+ }
99
+
100
+ // Check if evolution is available (sets flag in status for status line indicator)
101
+ const evolutionReady = checkEvolution(pokemon, state) !== null;
102
+
103
+ // Track XP toward next encounter and trigger if threshold met
104
+ state.xpSinceLastEncounter = (state.xpSinceLastEncounter ?? 0) + xpEvent.xp;
105
+ let encounterTriggered = false;
106
+
107
+ if (shouldTriggerEncounter(state.xpSinceLastEncounter) && !state.pendingEncounter) {
108
+ const encounter = generateEncounter(eventType, state);
109
+ if (encounter) {
110
+ state.pendingEncounter = encounter;
111
+ state.xpSinceLastEncounter = 0;
112
+ encounterTriggered = true;
113
+ }
114
+ }
115
+
116
+ // Single atomic save for all mutations
117
+ await stateManager.save();
118
+
119
+ // Write status for the status line (includes evolution flag and encounter notification)
120
+ if (encounterTriggered && state.pendingEncounter) {
121
+ const encSpecies = POKEMON_BY_ID.get(state.pendingEncounter.pokemonId);
122
+ const encName = encSpecies?.name ?? "???";
123
+ await stateManager.writeEncounterStatus(encName);
124
+ } else {
125
+ await stateManager.writeStatus(evolutionReady);
126
+ }
127
+
128
+ // Terminal bell on level-up, evolution available, or encounter (written to stderr so it reaches the terminal)
129
+ if ((levelUp || evolutionReady || encounterTriggered) && state.config.bellEnabled) {
130
+ process.stderr.write(BELL);
131
+ }