@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,295 @@
1
+ /**
2
+ * buddy_catch tool — Attempt to catch a wild Pokemon from a pending encounter.
3
+ * Preview mode checks requirements only; confirm mode rolls against catch rate.
4
+ */
5
+
6
+ import { z } from "zod";
7
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import type { OwnedPokemon, CodingStat, WildEncounter } from "../../engine/types.js";
9
+ import { POKEMON_BY_ID } from "../../engine/pokemon-data.js";
10
+ import { BELL, MAX_PARTY_SIZE, STAT_DISPLAY_NAMES } from "../../engine/constants.js";
11
+ import { initCodingStats } from "../../engine/stats.js";
12
+ import { canCatch } from "../../engine/encounters.js";
13
+ import { checkNewAchievements, unlockAchievement } from "../../gamification/achievements.js";
14
+ import { StateManager } from "../../state/state-manager.js";
15
+ import { formatTypes, pad } from "./display-helpers.js";
16
+
17
+ /** Check only the stat/level requirements without the catch-rate roll. */
18
+ function meetsRequirements(
19
+ encounter: WildEncounter,
20
+ activePokemon: OwnedPokemon,
21
+ ): { met: boolean; reason: string } {
22
+ const species = POKEMON_BY_ID.get(encounter.pokemonId);
23
+ if (!species) return { met: false, reason: "Unknown Pokemon" };
24
+
25
+ const { requiredStat, minStatValue, requiredLevel } = encounter.catchCondition;
26
+
27
+ if (activePokemon.level < requiredLevel) {
28
+ return {
29
+ met: false,
30
+ reason: `Need level ${requiredLevel} to catch ${species.rarity} Pokemon (currently level ${activePokemon.level})`,
31
+ };
32
+ }
33
+
34
+ if (requiredStat !== null) {
35
+ const currentStat = activePokemon.codingStats[requiredStat as CodingStat];
36
+ if (currentStat < minStatValue) {
37
+ return {
38
+ met: false,
39
+ reason: `Need ${requiredStat.toUpperCase()} at ${minStatValue} to catch this Pokemon (currently ${currentStat})`,
40
+ };
41
+ }
42
+ }
43
+
44
+ return { met: true, reason: "" };
45
+ }
46
+
47
+ /** Registers the buddy_catch tool on the MCP server. */
48
+ export function registerCatchTool(server: McpServer): void {
49
+ server.tool(
50
+ "buddy_catch",
51
+ "Attempt to catch a wild Pokemon from a pending encounter.",
52
+ { confirm: z.boolean().optional() },
53
+ async (params) => {
54
+ const stateManager = StateManager.getInstance();
55
+ const state = await stateManager.load();
56
+
57
+ if (!state || state.party.length === 0) {
58
+ return {
59
+ content: [
60
+ {
61
+ type: "text" as const,
62
+ text: "You don't have a Pokemon yet! Use buddy_starter to pick your first partner.",
63
+ },
64
+ ],
65
+ isError: true,
66
+ };
67
+ }
68
+
69
+ const encounter = state.pendingEncounter;
70
+ if (!encounter) {
71
+ return {
72
+ content: [
73
+ {
74
+ type: "text" as const,
75
+ text: "No wild Pokemon nearby! Keep coding and one will appear.",
76
+ },
77
+ ],
78
+ };
79
+ }
80
+
81
+ const species = POKEMON_BY_ID.get(encounter.pokemonId);
82
+ if (!species) {
83
+ stateManager.clearPendingEncounter();
84
+ await stateManager.save();
85
+ return {
86
+ content: [
87
+ {
88
+ type: "text" as const,
89
+ text: "The wild Pokemon vanished into the tall grass...",
90
+ },
91
+ ],
92
+ };
93
+ }
94
+
95
+ const active = stateManager.getActivePokemon();
96
+ if (!active) {
97
+ return {
98
+ content: [
99
+ {
100
+ type: "text" as const,
101
+ text: "No active Pokemon found in your party.",
102
+ },
103
+ ],
104
+ isError: true,
105
+ };
106
+ }
107
+
108
+ const typeStr = formatTypes(species.types);
109
+ const { catchCondition } = encounter;
110
+
111
+ // Build condition description
112
+ const conditionParts: string[] = [];
113
+ if (catchCondition.requiredLevel > 1) {
114
+ conditionParts.push(`Level ${catchCondition.requiredLevel}+`);
115
+ }
116
+ if (catchCondition.requiredStat !== null) {
117
+ const statName = STAT_DISPLAY_NAMES[catchCondition.requiredStat as CodingStat];
118
+ conditionParts.push(`${statName} >= ${catchCondition.minStatValue}`);
119
+ }
120
+ const conditionStr = conditionParts.length > 0 ? conditionParts.join(", ") : "None";
121
+
122
+ // Check stat/level requirements (does NOT roll catch rate)
123
+ const reqCheck = meetsRequirements(encounter, active);
124
+
125
+ // Requirements not met — Pokemon flees
126
+ if (!reqCheck.met) {
127
+ const W = 42;
128
+ const border = "\u2500".repeat(W);
129
+ const lines: string[] = [];
130
+
131
+ lines.push(`\u250c${border}\u2510`);
132
+ lines.push(`\u2502 ${pad(`Wild ${species.name} appeared!`, W - 2)}\u2502`);
133
+ lines.push(`\u2502 ${pad(`Type: ${typeStr} Lv.${encounter.level}`, W - 2)}\u2502`);
134
+ lines.push(`\u2502 ${pad(`Catch req: ${conditionStr}`, W - 2)}\u2502`);
135
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
136
+ lines.push(`\u2502 ${pad(reqCheck.reason, W - 2)}\u2502`);
137
+ lines.push(`\u2502 ${pad("It fled! Keep leveling up.", W - 2)}\u2502`);
138
+ lines.push(`\u2514${border}\u2518`);
139
+
140
+ // Clear encounter on flee
141
+ stateManager.clearPendingEncounter();
142
+ await stateManager.save();
143
+
144
+ return {
145
+ content: [{ type: "text" as const, text: lines.join("\n") }],
146
+ };
147
+ }
148
+
149
+ // Requirements met — show preview (no confirm) or attempt catch (confirm=true)
150
+ if (!params.confirm) {
151
+ const W = 42;
152
+ const border = "\u2500".repeat(W);
153
+ const lines: string[] = [];
154
+
155
+ lines.push(`\u250c${border}\u2510`);
156
+ lines.push(`\u2502 ${pad(`Wild ${species.name} appeared!`, W - 2)}\u2502`);
157
+ lines.push(`\u2502 ${pad(`Type: ${typeStr} Lv.${encounter.level}`, W - 2)}\u2502`);
158
+ lines.push(`\u2502 ${pad(`Catch req: ${conditionStr}`, W - 2)}\u2502`);
159
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
160
+ lines.push(`\u2502 ${pad(`Rarity: ${species.rarity}`, W - 2)}\u2502`);
161
+ lines.push(`\u2502 ${pad(`"${species.description}"`, W - 2)}\u2502`);
162
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
163
+ lines.push(`\u2502 ${pad("Use /buddy catch confirm to throw a Pokeball!", W - 2)}\u2502`);
164
+ lines.push(`\u2514${border}\u2518`);
165
+
166
+ return {
167
+ content: [{ type: "text" as const, text: lines.join("\n") }],
168
+ };
169
+ }
170
+
171
+ // Confirm — roll against catch rate via canCatch()
172
+ const catchResult = canCatch(encounter, active);
173
+
174
+ if (!catchResult.success) {
175
+ // Catch rate roll failed — broke free
176
+ const W = 42;
177
+ const border = "\u2500".repeat(W);
178
+ const lines: string[] = [];
179
+
180
+ lines.push(`\u250c${border}\u2510`);
181
+ lines.push(`\u2502 ${pad(`You threw a Pokeball at ${species.name}!`, W - 2)}\u2502`);
182
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
183
+ lines.push(`\u2502 ${pad(catchResult.reason, W - 2)}\u2502`);
184
+ lines.push(`\u2502 ${pad("Better luck next time...", W - 2)}\u2502`);
185
+ lines.push(`\u2514${border}\u2518`);
186
+
187
+ // Clear encounter on failed catch
188
+ stateManager.clearPendingEncounter();
189
+ await stateManager.save();
190
+
191
+ return {
192
+ content: [{ type: "text" as const, text: lines.join("\n") }],
193
+ };
194
+ }
195
+
196
+ // Catch succeeded — create OwnedPokemon
197
+ const now = new Date().toISOString();
198
+ const newPokemon: OwnedPokemon = {
199
+ id: crypto.randomUUID(),
200
+ pokemonId: encounter.pokemonId,
201
+ nickname: null,
202
+ level: encounter.level,
203
+ currentXp: 0,
204
+ totalXp: 0,
205
+ codingStats: initCodingStats(species.baseStats),
206
+ happiness: 70,
207
+ caughtAt: now,
208
+ evolvedAt: null,
209
+ isActive: false,
210
+ personality: null,
211
+ shiny: false,
212
+ isStarter: false,
213
+ };
214
+
215
+ // Add to party or PC box
216
+ let placedIn: "party" | "pcBox";
217
+ if (state.party.length < MAX_PARTY_SIZE) {
218
+ state.party.push(newPokemon);
219
+ placedIn = "party";
220
+ } else {
221
+ state.pcBox.push(newPokemon);
222
+ placedIn = "pcBox";
223
+ }
224
+
225
+ // Update pokedex
226
+ const existingEntry = state.pokedex.entries[encounter.pokemonId];
227
+ if (existingEntry) {
228
+ if (!existingEntry.caught) {
229
+ existingEntry.caught = true;
230
+ existingEntry.firstCaught = now;
231
+ state.pokedex.totalCaught += 1;
232
+ }
233
+ } else {
234
+ state.pokedex.entries[encounter.pokemonId] = {
235
+ seen: true,
236
+ caught: true,
237
+ firstSeen: now,
238
+ firstCaught: now,
239
+ };
240
+ state.pokedex.totalSeen += 1;
241
+ state.pokedex.totalCaught += 1;
242
+ }
243
+
244
+ // Clear pending encounter
245
+ stateManager.clearPendingEncounter();
246
+
247
+ // Check for new achievements
248
+ const newAchievements = checkNewAchievements(state);
249
+ for (const achievement of newAchievements) {
250
+ state.achievements.push(unlockAchievement(achievement.id));
251
+ }
252
+
253
+ // Save state
254
+ await stateManager.save();
255
+ await stateManager.writeStatus();
256
+
257
+ // Build celebration message
258
+ const W = 42;
259
+ const border = "\u2500".repeat(W);
260
+ const lines: string[] = [];
261
+
262
+ lines.push(`\u250c${border}\u2510`);
263
+ lines.push(`\u2502 ${pad(`Gotcha! ${species.name} was caught!`, W - 2)}\u2502`);
264
+ lines.push(`\u2502 ${pad(`Type: ${typeStr} Lv.${encounter.level}`, W - 2)}\u2502`);
265
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
266
+
267
+ if (placedIn === "party") {
268
+ lines.push(`\u2502 ${pad(`${species.name} joined your party!`, W - 2)}\u2502`);
269
+ } else {
270
+ lines.push(`\u2502 ${pad(`Party full! Sent to PC Box.`, W - 2)}\u2502`);
271
+ }
272
+
273
+ lines.push(`\u2502 ${pad(`Pokedex: ${state.pokedex.totalCaught}/151 caught`, W - 2)}\u2502`);
274
+ lines.push(`\u2514${border}\u2518`);
275
+
276
+ // Show new achievements
277
+ if (newAchievements.length > 0) {
278
+ lines.push("");
279
+ for (const achievement of newAchievements) {
280
+ lines.push(`*** Achievement unlocked: ${achievement.name}! ***`);
281
+ lines.push(` ${achievement.description}`);
282
+ }
283
+ }
284
+
285
+ // Terminal bell
286
+ if (state.config.bellEnabled) {
287
+ process.stderr.write(BELL);
288
+ }
289
+
290
+ return {
291
+ content: [{ type: "text" as const, text: lines.join("\n") }],
292
+ };
293
+ },
294
+ );
295
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Shared display helper functions used across multiple tool files.
3
+ * Centralizes formatting logic to eliminate duplication.
4
+ */
5
+
6
+ import type { CodingStat, BaseStats } from "../../engine/types.js";
7
+ import { BASE_STAT_TO_CODING } from "../../engine/types.js";
8
+
9
+ /** Format a Pokemon type array for display (e.g., "Fire/Flying"). */
10
+ export function formatTypes(types: readonly [string, string?]): string {
11
+ return types.filter(Boolean).join("/");
12
+ }
13
+
14
+ /** Right-pad a string to a fixed width, truncating if needed. */
15
+ export function pad(str: string, width: number): string {
16
+ if (str.length >= width) {
17
+ return str.slice(0, width);
18
+ }
19
+ return str + " ".repeat(width - str.length);
20
+ }
21
+
22
+ /** Render a compact XP progress bar using Unicode block characters. */
23
+ export function renderXpBar(percent: number, width: number = 20): string {
24
+ const filled = Math.round((percent / 100) * width);
25
+ const empty = width - filled;
26
+ return "\u2588".repeat(filled) + "\u2591".repeat(empty);
27
+ }
28
+
29
+ /**
30
+ * Inverted BASE_STAT_TO_CODING map: coding stat -> base stat key.
31
+ * Used for stat comparison during evolution preview and Pokedex display.
32
+ */
33
+ export const CODING_TO_BASE: Record<CodingStat, keyof BaseStats> = Object.fromEntries(
34
+ Object.entries(BASE_STAT_TO_CODING).map(([base, coding]) => [coding, base]),
35
+ ) as Record<CodingStat, keyof BaseStats>;
@@ -0,0 +1,236 @@
1
+ /**
2
+ * buddy_evolve tool — Evolve the active Pokemon when eligible.
3
+ * Shows a preview with stat comparison, or applies the evolution on confirm.
4
+ */
5
+
6
+ import { z } from "zod";
7
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import type { EvolutionMethod, CodingStat } from "../../engine/types.js";
9
+ import { CODING_STATS } from "../../engine/types.js";
10
+ import { POKEMON_BY_ID } from "../../engine/pokemon-data.js";
11
+ import { STAT_DISPLAY_NAMES, BELL, BADGES } from "../../engine/constants.js";
12
+ import { renderStatBar } from "../../engine/stats.js";
13
+ import {
14
+ checkEvolution,
15
+ applyEvolution,
16
+ getEvolutionLinks,
17
+ getNewlyEarnedBadges,
18
+ } from "../../engine/evolution.js";
19
+ import { StateManager } from "../../state/state-manager.js";
20
+ import { formatTypes, pad, CODING_TO_BASE } from "./display-helpers.js";
21
+
22
+ /** Describe an evolution method in human-readable text. */
23
+ function describeMethod(method: EvolutionMethod): string {
24
+ switch (method.type) {
25
+ case "level":
26
+ return `Reach Lv.${method.level}`;
27
+ case "badge": {
28
+ const badgeDef = BADGES.find((b) => b.type === method.badge);
29
+ const badgeName = badgeDef ? badgeDef.name : method.badge;
30
+ return `Earn the ${badgeName}`;
31
+ }
32
+ case "collaboration":
33
+ return "Merge 10 PRs (collaboration evolution)";
34
+ case "stat":
35
+ return `Reach Lv.25 with ${STAT_DISPLAY_NAMES[method.stat]} >= ${method.minValue}`;
36
+ }
37
+ }
38
+
39
+ /** Registers the buddy_evolve tool on the MCP server. */
40
+ export function registerEvolveTool(server: McpServer): void {
41
+ server.tool(
42
+ "buddy_evolve",
43
+ "Evolve your active Pokemon if it meets the evolution requirements.",
44
+ { confirm: z.boolean().optional() },
45
+ async (params) => {
46
+ const stateManager = StateManager.getInstance();
47
+ const state = await stateManager.load();
48
+
49
+ if (!state || state.party.length === 0) {
50
+ return {
51
+ content: [
52
+ {
53
+ type: "text" as const,
54
+ text: "You don't have a Pokemon yet! Use buddy_starter to pick your first partner.",
55
+ },
56
+ ],
57
+ isError: true,
58
+ };
59
+ }
60
+
61
+ const active = stateManager.getActivePokemon();
62
+ if (!active) {
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text" as const,
67
+ text: "No active Pokemon found in your party.",
68
+ },
69
+ ],
70
+ isError: true,
71
+ };
72
+ }
73
+
74
+ const species = POKEMON_BY_ID.get(active.pokemonId);
75
+ if (!species) {
76
+ return {
77
+ content: [
78
+ {
79
+ type: "text" as const,
80
+ text: `Could not find species data for Pokemon ID ${active.pokemonId}.`,
81
+ },
82
+ ],
83
+ isError: true,
84
+ };
85
+ }
86
+
87
+ // Check for newly earned badges before evolution check
88
+ const newBadges = getNewlyEarnedBadges(state);
89
+ const badgeLines: string[] = [];
90
+ if (newBadges.length > 0) {
91
+ for (const badge of newBadges) {
92
+ state.badges.push(badge);
93
+ const badgeDef = BADGES.find((b) => b.type === badge);
94
+ const badgeName = badgeDef ? badgeDef.name : badge;
95
+ badgeLines.push(`*** New badge earned: ${badgeName}! ***`);
96
+ }
97
+ await stateManager.save();
98
+ }
99
+
100
+ // Check evolution eligibility
101
+ const eligibleLink = checkEvolution(active, state);
102
+
103
+ if (!eligibleLink) {
104
+ // Not eligible — explain what's needed
105
+ const links = getEvolutionLinks(active.pokemonId);
106
+ const displayName = active.nickname ?? species.name;
107
+ const lines: string[] = [];
108
+
109
+ if (badgeLines.length > 0) {
110
+ lines.push(...badgeLines, "");
111
+ }
112
+
113
+ if (links.length === 0) {
114
+ lines.push(`${displayName} does not evolve.`);
115
+ } else {
116
+ lines.push(`${displayName} is not ready to evolve yet.`);
117
+ lines.push("");
118
+ lines.push("Evolution requirements:");
119
+ for (const link of links) {
120
+ const target = POKEMON_BY_ID.get(link.to);
121
+ const targetName = target ? target.name : `#${link.to}`;
122
+ lines.push(` -> ${targetName}: ${describeMethod(link.method)}`);
123
+ }
124
+ lines.push("");
125
+ lines.push(`Current level: ${active.level}`);
126
+ }
127
+
128
+ return {
129
+ content: [{ type: "text" as const, text: lines.join("\n") }],
130
+ };
131
+ }
132
+
133
+ const targetSpecies = POKEMON_BY_ID.get(eligibleLink.to);
134
+ if (!targetSpecies) {
135
+ return {
136
+ content: [
137
+ {
138
+ type: "text" as const,
139
+ text: `Could not find target species data for Pokemon ID ${eligibleLink.to}.`,
140
+ },
141
+ ],
142
+ isError: true,
143
+ };
144
+ }
145
+
146
+ const displayName = active.nickname ?? species.name;
147
+
148
+ // Preview mode: show what the evolution will look like
149
+ if (!params.confirm) {
150
+ const W = 42;
151
+ const border = "\u2500".repeat(W);
152
+ const lines: string[] = [];
153
+
154
+ if (badgeLines.length > 0) {
155
+ lines.push(...badgeLines, "");
156
+ }
157
+
158
+ lines.push(`\u250c${border}\u2510`);
159
+ lines.push(`\u2502 ${pad(`What? ${displayName} is evolving!`, W - 2)}\u2502`);
160
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
161
+ lines.push(`\u2502 ${pad(`${species.name} \u2192 ${targetSpecies.name}`, W - 2)}\u2502`);
162
+ lines.push(
163
+ `\u2502 ${pad(`${formatTypes(species.types)} ${formatTypes(targetSpecies.types)}`, W - 2)}\u2502`,
164
+ );
165
+ lines.push(`\u2502 ${pad(`Lv.${active.level}`, W - 2)}\u2502`);
166
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
167
+ lines.push(`\u2502 ${pad("New base stats:", W - 2)}\u2502`);
168
+
169
+ // Show stat comparison (new base vs current)
170
+ for (const stat of CODING_STATS) {
171
+ const currentValue = active.codingStats[stat as CodingStat];
172
+ const label = STAT_DISPLAY_NAMES[stat as CodingStat].padEnd(10);
173
+ // Calculate what new stat would be after evolution (preview)
174
+ const oldBaseContrib = Math.floor(
175
+ species.baseStats[CODING_TO_BASE[stat as CodingStat]] * 0.5,
176
+ );
177
+ const activityBonus = Math.max(0, currentValue - oldBaseContrib);
178
+ const newBaseContrib = Math.floor(
179
+ targetSpecies.baseStats[CODING_TO_BASE[stat as CodingStat]] * 0.5,
180
+ );
181
+ const newValue = newBaseContrib + activityBonus;
182
+ const bar = renderStatBar(newValue);
183
+ const delta = newValue !== currentValue ? ` (was ${currentValue})` : "";
184
+ lines.push(
185
+ `\u2502 ${pad(`${label} ${bar} ${String(newValue).padStart(3)}${delta}`, W - 2)}\u2502`,
186
+ );
187
+ }
188
+
189
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
190
+ lines.push(`\u2502 ${pad("Use /buddy evolve confirm to proceed", W - 2)}\u2502`);
191
+ lines.push(`\u2502 ${pad("(or don't -- like pressing B!)", W - 2)}\u2502`);
192
+ lines.push(`\u2514${border}\u2518`);
193
+
194
+ return {
195
+ content: [{ type: "text" as const, text: lines.join("\n") }],
196
+ };
197
+ }
198
+
199
+ // Confirm mode: apply evolution
200
+ const { newName, newTypes } = applyEvolution(active, eligibleLink.to);
201
+
202
+ // Save state
203
+ await stateManager.save();
204
+ await stateManager.writeStatus();
205
+
206
+ // Build celebration message
207
+ const lines: string[] = [];
208
+
209
+ if (badgeLines.length > 0) {
210
+ lines.push(...badgeLines, "");
211
+ }
212
+
213
+ lines.push(`Congratulations! ${displayName} evolved into ${newName}!`);
214
+ lines.push(`Type: ${formatTypes(newTypes as readonly [string, string?])}`);
215
+ lines.push("");
216
+
217
+ // Show new stats
218
+ lines.push("Updated stats:");
219
+ for (const stat of CODING_STATS) {
220
+ const value = active.codingStats[stat as CodingStat];
221
+ const label = STAT_DISPLAY_NAMES[stat as CodingStat].padEnd(10);
222
+ const bar = renderStatBar(value);
223
+ lines.push(` ${label} ${bar} ${String(value).padStart(3)}`);
224
+ }
225
+
226
+ // Terminal bell
227
+ if (state.config.bellEnabled) {
228
+ process.stderr.write(BELL);
229
+ }
230
+
231
+ return {
232
+ content: [{ type: "text" as const, text: lines.join("\n") }],
233
+ };
234
+ },
235
+ );
236
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * buddy_legendary tool — Display progress on all 5 legendary quest chains.
3
+ * Each quest has multi-step conditions tied to sustained coding activity.
4
+ */
5
+
6
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { POKEMON_BY_ID } from "../../engine/pokemon-data.js";
8
+ import { getQuestProgress } from "../../gamification/legendary-quests.js";
9
+ import { StateManager } from "../../state/state-manager.js";
10
+ import { pad } from "./display-helpers.js";
11
+
12
+ /** Registers the buddy_legendary tool on the MCP server. */
13
+ export function registerLegendaryTool(server: McpServer): void {
14
+ server.tool(
15
+ "buddy_legendary",
16
+ "View progress on all legendary Pokemon quest chains.",
17
+ {},
18
+ async () => {
19
+ const stateManager = StateManager.getInstance();
20
+ const state = await stateManager.load();
21
+
22
+ if (!state) {
23
+ return {
24
+ content: [
25
+ {
26
+ type: "text" as const,
27
+ text: "You don't have a Pokemon yet! Use buddy_starter to begin your journey.",
28
+ },
29
+ ],
30
+ isError: true,
31
+ };
32
+ }
33
+
34
+ const progress = getQuestProgress(state);
35
+
36
+ const W = 48;
37
+ const border = "\u2500".repeat(W);
38
+ const lines: string[] = [];
39
+
40
+ lines.push("L E G E N D A R Y Q U E S T S");
41
+ lines.push("");
42
+
43
+ for (const entry of progress) {
44
+ const { quest, stepsCompleted, totalSteps } = entry;
45
+ const species = POKEMON_BY_ID.get(quest.pokemonId);
46
+ const pokemonName = species?.name ?? `#${quest.pokemonId}`;
47
+
48
+ lines.push(
49
+ `\u250c\u2500 LEGENDARY QUEST: ${pokemonName} ${"\u2500".repeat(Math.max(0, W - 24 - pokemonName.length))}\u2510`,
50
+ );
51
+ lines.push(`\u2502 ${pad(quest.name, W - 2)}\u2502`);
52
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
53
+
54
+ for (let i = 0; i < quest.steps.length; i++) {
55
+ const step = quest.steps[i]!;
56
+ const completed = i < stepsCompleted;
57
+ const marker = completed ? "\u2713" : "\u25cb";
58
+ const stepText = `Step ${i + 1}: ${marker} ${step.description}`;
59
+ lines.push(`\u2502 ${pad(stepText, W - 2)}\u2502`);
60
+ }
61
+
62
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
63
+
64
+ const allDone = stepsCompleted === totalSteps;
65
+ const progressStr = allDone
66
+ ? `COMPLETE! ${pokemonName} awaits you!`
67
+ : `Progress: ${stepsCompleted}/${totalSteps}`;
68
+ lines.push(`\u2502 ${pad(progressStr, W - 2)}\u2502`);
69
+ lines.push(`\u2514${border}\u2518`);
70
+ lines.push("");
71
+ }
72
+
73
+ return {
74
+ content: [{ type: "text" as const, text: lines.join("\n") }],
75
+ };
76
+ },
77
+ );
78
+ }