@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,251 @@
1
+ /**
2
+ * buddy_party tool — Manage your party of up to 6 Pokemon.
3
+ * Supports listing, switching active, depositing to PC, and withdrawing from PC.
4
+ */
5
+
6
+ import { z } from "zod";
7
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { POKEMON_BY_ID } from "../../engine/pokemon-data.js";
9
+ import { MAX_PARTY_SIZE } from "../../engine/constants.js";
10
+ import { StateManager } from "../../state/state-manager.js";
11
+ import { formatTypes, pad } from "./display-helpers.js";
12
+
13
+ /** Registers the buddy_party tool on the MCP server. */
14
+ export function registerPartyTool(server: McpServer): void {
15
+ server.tool(
16
+ "buddy_party",
17
+ "Manage your Pokemon party: list members, switch active, deposit to PC, or withdraw from PC.",
18
+ {
19
+ action: z.enum(["list", "switch", "deposit", "withdraw"]).optional(),
20
+ slot: z.number().int().optional(),
21
+ },
22
+ async (params) => {
23
+ const stateManager = StateManager.getInstance();
24
+ const state = await stateManager.load();
25
+
26
+ if (!state || state.party.length === 0) {
27
+ return {
28
+ content: [
29
+ {
30
+ type: "text" as const,
31
+ text: "You don't have a Pokemon yet! Use buddy_starter to pick your first partner.",
32
+ },
33
+ ],
34
+ isError: true,
35
+ };
36
+ }
37
+
38
+ const action = params.action ?? "list";
39
+
40
+ // ── LIST ────────────────────────────────────────────────
41
+ if (action === "list") {
42
+ const W = 42;
43
+ const border = "\u2500".repeat(W);
44
+ const lines: string[] = [];
45
+
46
+ lines.push(`\u250c${border}\u2510`);
47
+ lines.push(`\u2502 ${pad("YOUR PARTY", W - 2)}\u2502`);
48
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
49
+
50
+ for (let i = 0; i < state.party.length; i++) {
51
+ const pokemon = state.party[i]!;
52
+ const species = POKEMON_BY_ID.get(pokemon.pokemonId);
53
+ if (!species) continue;
54
+
55
+ const displayName = pokemon.nickname ?? species.name;
56
+ const typeStr = formatTypes(species.types);
57
+ const marker = pokemon.isActive ? " \u2605" : "";
58
+ const entry = `${i + 1}. ${displayName} Lv.${pokemon.level} ${typeStr}${marker}`;
59
+ lines.push(`\u2502 ${pad(entry, W - 2)}\u2502`);
60
+ }
61
+
62
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
63
+ lines.push(`\u2502 ${pad(`Party: ${state.party.length}/${MAX_PARTY_SIZE}`, W - 2)}\u2502`);
64
+
65
+ if (state.pcBox.length > 0) {
66
+ lines.push(`\u2502 ${pad(`PC Box: ${state.pcBox.length} Pokemon`, W - 2)}\u2502`);
67
+ }
68
+
69
+ lines.push(`\u2514${border}\u2518`);
70
+
71
+ return {
72
+ content: [{ type: "text" as const, text: lines.join("\n") }],
73
+ };
74
+ }
75
+
76
+ // ── SWITCH ──────────────────────────────────────────────
77
+ if (action === "switch") {
78
+ const slot = params.slot;
79
+ if (slot === undefined || slot < 1 || slot > state.party.length) {
80
+ return {
81
+ content: [
82
+ {
83
+ type: "text" as const,
84
+ text: `Invalid slot. Choose a number between 1 and ${state.party.length}.`,
85
+ },
86
+ ],
87
+ isError: true,
88
+ };
89
+ }
90
+
91
+ const target = state.party[slot - 1]!;
92
+ if (target.isActive) {
93
+ const species = POKEMON_BY_ID.get(target.pokemonId);
94
+ const displayName = target.nickname ?? species?.name ?? "???";
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text" as const,
99
+ text: `${displayName} is already your active Pokemon!`,
100
+ },
101
+ ],
102
+ };
103
+ }
104
+
105
+ // Deactivate all, activate target
106
+ for (const pokemon of state.party) {
107
+ pokemon.isActive = false;
108
+ }
109
+ target.isActive = true;
110
+
111
+ await stateManager.save();
112
+ await stateManager.writeStatus();
113
+
114
+ const species = POKEMON_BY_ID.get(target.pokemonId);
115
+ const displayName = target.nickname ?? species?.name ?? "???";
116
+ const typeStr = species ? formatTypes(species.types) : "";
117
+
118
+ const lines: string[] = [];
119
+ lines.push(`Go, ${displayName}!`);
120
+ lines.push(`Lv.${target.level} ${typeStr}`);
121
+
122
+ return {
123
+ content: [{ type: "text" as const, text: lines.join("\n") }],
124
+ };
125
+ }
126
+
127
+ // ── DEPOSIT ─────────────────────────────────────────────
128
+ if (action === "deposit") {
129
+ const slot = params.slot;
130
+ if (slot === undefined || slot < 1 || slot > state.party.length) {
131
+ return {
132
+ content: [
133
+ {
134
+ type: "text" as const,
135
+ text: `Invalid slot. Choose a number between 1 and ${state.party.length}.`,
136
+ },
137
+ ],
138
+ isError: true,
139
+ };
140
+ }
141
+
142
+ if (state.party.length <= 1) {
143
+ return {
144
+ content: [
145
+ {
146
+ type: "text" as const,
147
+ text: "You can't deposit your last Pokemon! You need at least one in your party.",
148
+ },
149
+ ],
150
+ isError: true,
151
+ };
152
+ }
153
+
154
+ const deposited = state.party.splice(slot - 1, 1)[0]!;
155
+ const wasActive = deposited.isActive;
156
+ deposited.isActive = false;
157
+ state.pcBox.push(deposited);
158
+
159
+ // If the deposited Pokemon was active, set the first party member as active
160
+ if (wasActive && state.party.length > 0) {
161
+ state.party[0]!.isActive = true;
162
+ }
163
+
164
+ await stateManager.save();
165
+ await stateManager.writeStatus();
166
+
167
+ const species = POKEMON_BY_ID.get(deposited.pokemonId);
168
+ const displayName = deposited.nickname ?? species?.name ?? "???";
169
+
170
+ const lines: string[] = [];
171
+ lines.push(`${displayName} was deposited in the PC Box.`);
172
+ lines.push(
173
+ `Party: ${state.party.length}/${MAX_PARTY_SIZE} | PC Box: ${state.pcBox.length}`,
174
+ );
175
+
176
+ if (wasActive) {
177
+ const newActive = state.party[0]!;
178
+ const newSpecies = POKEMON_BY_ID.get(newActive.pokemonId);
179
+ const newName = newActive.nickname ?? newSpecies?.name ?? "???";
180
+ lines.push(`${newName} is now your active Pokemon.`);
181
+ }
182
+
183
+ return {
184
+ content: [{ type: "text" as const, text: lines.join("\n") }],
185
+ };
186
+ }
187
+
188
+ // ── WITHDRAW ────────────────────────────────────────────
189
+ if (action === "withdraw") {
190
+ const slot = params.slot;
191
+ if (slot === undefined || slot < 1 || slot > state.pcBox.length) {
192
+ if (state.pcBox.length === 0) {
193
+ return {
194
+ content: [
195
+ {
196
+ type: "text" as const,
197
+ text: "Your PC Box is empty! Catch more Pokemon to fill it.",
198
+ },
199
+ ],
200
+ };
201
+ }
202
+ return {
203
+ content: [
204
+ {
205
+ type: "text" as const,
206
+ text: `Invalid slot. Choose a number between 1 and ${state.pcBox.length}.`,
207
+ },
208
+ ],
209
+ isError: true,
210
+ };
211
+ }
212
+
213
+ if (state.party.length >= MAX_PARTY_SIZE) {
214
+ return {
215
+ content: [
216
+ {
217
+ type: "text" as const,
218
+ text: `Your party is full (${MAX_PARTY_SIZE}/${MAX_PARTY_SIZE}). Deposit a Pokemon first.`,
219
+ },
220
+ ],
221
+ isError: true,
222
+ };
223
+ }
224
+
225
+ const withdrawn = state.pcBox.splice(slot - 1, 1)[0]!;
226
+ state.party.push(withdrawn);
227
+
228
+ await stateManager.save();
229
+
230
+ const species = POKEMON_BY_ID.get(withdrawn.pokemonId);
231
+ const displayName = withdrawn.nickname ?? species?.name ?? "???";
232
+
233
+ const lines: string[] = [];
234
+ lines.push(`${displayName} was withdrawn from the PC Box!`);
235
+ lines.push(
236
+ `Party: ${state.party.length}/${MAX_PARTY_SIZE} | PC Box: ${state.pcBox.length}`,
237
+ );
238
+
239
+ return {
240
+ content: [{ type: "text" as const, text: lines.join("\n") }],
241
+ };
242
+ }
243
+
244
+ // Should not reach here, but TypeScript exhaustiveness
245
+ return {
246
+ content: [{ type: "text" as const, text: "Unknown party action." }],
247
+ isError: true,
248
+ };
249
+ },
250
+ );
251
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * buddy_pet tool — Pet your active Pokemon for a small happiness/XP boost.
3
+ * Returns a type-flavored reaction message.
4
+ */
5
+
6
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import type { PokemonType, Pokemon, OwnedPokemon } from "../../engine/types.js";
8
+ import { POKEMON_BY_ID } from "../../engine/pokemon-data.js";
9
+ import { MAX_HAPPINESS } from "../../engine/constants.js";
10
+ import { addXp, createXpEvent } from "../../engine/xp.js";
11
+ import { applyStatBoost } from "../../engine/stats.js";
12
+ import { StateManager } from "../../state/state-manager.js";
13
+
14
+ /** Pet reactions keyed by primary Pokemon type. */
15
+ const PET_REACTIONS: Record<PokemonType, (name: string) => string> = {
16
+ Normal: (n) => `*${n} nuzzles against your hand contentedly*`,
17
+ Fire: (n) => `*${n}'s tail flame flickers happily*`,
18
+ Water: (n) => `*${n} splashes with joy*`,
19
+ Electric: (n) => `*${n}'s cheeks spark warmly*`,
20
+ Grass: (n) => `*${n}'s leaves rustle with delight*`,
21
+ Ice: (n) => `*${n} breathes a cool, contented mist*`,
22
+ Fighting: (n) => `*${n} flexes proudly and grins*`,
23
+ Poison: (n) => `*${n} oozes happily... you wash your hands*`,
24
+ Ground: (n) => `*${n} stomps the ground with glee*`,
25
+ Flying: (n) => `*${n} flutters around you in circles*`,
26
+ Psychic: (n) => `*${n}'s eyes glow softly with gratitude*`,
27
+ Bug: (n) => `*${n}'s antennae twitch happily*`,
28
+ Rock: (n) => `*${n} rumbles warmly, solid as ever*`,
29
+ Ghost: (n) => `*${n} phases through your hand... then comes back for more*`,
30
+ Dragon: (n) => `*${n} lets out a low, pleased growl*`,
31
+ };
32
+
33
+ /** Get a pet reaction based on the Pokemon's primary type. */
34
+ function getPetReaction(species: Pokemon, displayName: string): string {
35
+ const primaryType = species.types[0];
36
+ const reactionFn = PET_REACTIONS[primaryType];
37
+ return reactionFn(displayName);
38
+ }
39
+
40
+ /** Registers the buddy_pet tool on the MCP server. */
41
+ export function registerPetTool(server: McpServer): void {
42
+ server.tool(
43
+ "buddy_pet",
44
+ "Pet your active Pokemon to increase happiness and earn a small XP bonus.",
45
+ {},
46
+ async () => {
47
+ const stateManager = StateManager.getInstance();
48
+ const state = await stateManager.load();
49
+
50
+ if (!state || state.party.length === 0) {
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text" as const,
55
+ text: "You don't have a Pokemon yet! Use buddy_starter to pick your first partner.",
56
+ },
57
+ ],
58
+ isError: true,
59
+ };
60
+ }
61
+
62
+ const active = stateManager.getActivePokemon();
63
+ if (!active) {
64
+ return {
65
+ content: [
66
+ {
67
+ type: "text" as const,
68
+ text: "No active Pokemon found in your party.",
69
+ },
70
+ ],
71
+ isError: true,
72
+ };
73
+ }
74
+
75
+ const species = POKEMON_BY_ID.get(active.pokemonId);
76
+ if (!species) {
77
+ return {
78
+ content: [
79
+ {
80
+ type: "text" as const,
81
+ text: `Could not find species data for Pokemon ID ${active.pokemonId}.`,
82
+ },
83
+ ],
84
+ isError: true,
85
+ };
86
+ }
87
+
88
+ const displayName = active.nickname ?? species.name;
89
+
90
+ // Increase happiness (cap at 255)
91
+ const previousHappiness = active.happiness;
92
+ active.happiness = Math.min(MAX_HAPPINESS, active.happiness + 5);
93
+
94
+ // Award small XP via pet event
95
+ const xpEvent = createXpEvent("pet");
96
+ const levelUp = addXp(active, xpEvent.xp, species);
97
+
98
+ // Apply stat boost if the event has one
99
+ if (xpEvent.statBoost !== null && xpEvent.boostAmount > 0) {
100
+ applyStatBoost(active, xpEvent.statBoost, xpEvent.boostAmount);
101
+ }
102
+
103
+ // Save state and update status line
104
+ await stateManager.save();
105
+ await stateManager.writeStatus();
106
+
107
+ // Build response
108
+ const reaction = getPetReaction(species, displayName);
109
+ const lines: string[] = [reaction, ""];
110
+
111
+ lines.push(`Happiness: ${previousHappiness} -> ${active.happiness}`);
112
+ lines.push(`+${xpEvent.xp} XP`);
113
+
114
+ if (levelUp) {
115
+ lines.push("");
116
+ lines.push(`*** ${displayName} grew to Lv.${levelUp.newLevel}! ***`);
117
+ }
118
+
119
+ return {
120
+ content: [{ type: "text" as const, text: lines.join("\n") }],
121
+ };
122
+ },
123
+ );
124
+ }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * buddy_pokedex tool — Browse the Pokedex: view all 151, filter by caught/seen,
3
+ * or inspect a specific Pokemon's details.
4
+ */
5
+
6
+ import { z } from "zod";
7
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import type { CodingStat } from "../../engine/types.js";
9
+ import { CODING_STATS } from "../../engine/types.js";
10
+ import { POKEDEX, POKEMON_BY_ID } from "../../engine/pokemon-data.js";
11
+ import { STAT_DISPLAY_NAMES, TOTAL_POKEMON } from "../../engine/constants.js";
12
+ import { renderStatBar } from "../../engine/stats.js";
13
+ import { findEvolutionChain } from "../../engine/evolution.js";
14
+ import { StateManager } from "../../state/state-manager.js";
15
+ import { formatTypes, pad, CODING_TO_BASE } from "./display-helpers.js";
16
+
17
+ /** Registers the buddy_pokedex tool on the MCP server. */
18
+ export function registerPokedexTool(server: McpServer): void {
19
+ server.tool(
20
+ "buddy_pokedex",
21
+ "Browse the Pokedex: view all 151 Pokemon, filter by caught/seen, or look up a specific Pokemon.",
22
+ {
23
+ filter: z.enum(["all", "caught", "seen"]).optional(),
24
+ pokemon: z.string().optional(),
25
+ },
26
+ async (params) => {
27
+ const stateManager = StateManager.getInstance();
28
+ const state = await stateManager.load();
29
+
30
+ if (!state) {
31
+ return {
32
+ content: [
33
+ {
34
+ type: "text" as const,
35
+ text: "You don't have a Pokemon yet! Use buddy_starter to begin your journey.",
36
+ },
37
+ ],
38
+ isError: true,
39
+ };
40
+ }
41
+
42
+ // ── Specific Pokemon lookup ────────────────────────────
43
+ if (params.pokemon) {
44
+ return handlePokemonDetail(params.pokemon, state, stateManager);
45
+ }
46
+
47
+ const filter = params.filter ?? "all";
48
+
49
+ // ── ALL: Grid of 151 with symbols ──────────────────────
50
+ if (filter === "all") {
51
+ const lines: string[] = [];
52
+ lines.push("P O K E D E X");
53
+ lines.push(
54
+ `Caught: ${state.pokedex.totalCaught}/${TOTAL_POKEMON} | Seen: ${state.pokedex.totalSeen}/${TOTAL_POKEMON}`,
55
+ );
56
+ lines.push("");
57
+
58
+ // Render grid: 10 per row
59
+ const COLS = 10;
60
+ let row: string[] = [];
61
+
62
+ for (let i = 1; i <= TOTAL_POKEMON; i++) {
63
+ const entry = state.pokedex.entries[i];
64
+ let symbol: string;
65
+
66
+ if (entry?.caught) {
67
+ symbol = "\u25cf"; // ● caught
68
+ } else if (entry?.seen) {
69
+ symbol = "\u25d0"; // ◐ seen
70
+ } else {
71
+ symbol = "\u25cb"; // ○ unknown
72
+ }
73
+
74
+ const num = String(i).padStart(3, "0");
75
+ row.push(`${num}${symbol}`);
76
+
77
+ if (i % COLS === 0 || i === TOTAL_POKEMON) {
78
+ lines.push(row.join(" "));
79
+ row = [];
80
+ }
81
+ }
82
+
83
+ lines.push("");
84
+ lines.push("\u25cf = caught | \u25d0 = seen | \u25cb = unknown");
85
+
86
+ return {
87
+ content: [{ type: "text" as const, text: lines.join("\n") }],
88
+ };
89
+ }
90
+
91
+ // ── CAUGHT: Show only caught Pokemon ───────────────────
92
+ if (filter === "caught") {
93
+ if (state.pokedex.totalCaught === 0) {
94
+ return {
95
+ content: [
96
+ {
97
+ type: "text" as const,
98
+ text: "You haven't caught any Pokemon yet! Keep coding and encounter wild Pokemon.",
99
+ },
100
+ ],
101
+ };
102
+ }
103
+
104
+ const lines: string[] = [];
105
+ lines.push(`CAUGHT POKEMON (${state.pokedex.totalCaught}/${TOTAL_POKEMON})`);
106
+ lines.push("");
107
+
108
+ for (const [idStr, entry] of Object.entries(state.pokedex.entries)) {
109
+ if (!entry.caught) continue;
110
+ const pokemonId = Number(idStr);
111
+ const species = POKEMON_BY_ID.get(pokemonId);
112
+ if (!species) continue;
113
+
114
+ // Find the owned instance for level info
115
+ const allOwned = [...state.party, ...state.pcBox];
116
+ const owned = allOwned.find((p) => p.pokemonId === pokemonId);
117
+ const levelStr = owned ? `Lv.${owned.level}` : "";
118
+ const typeStr = formatTypes(species.types);
119
+ const num = String(pokemonId).padStart(3, "0");
120
+
121
+ lines.push(`#${num} ${species.name.padEnd(12)} ${typeStr.padEnd(16)} ${levelStr}`);
122
+ }
123
+
124
+ return {
125
+ content: [{ type: "text" as const, text: lines.join("\n") }],
126
+ };
127
+ }
128
+
129
+ // ── SEEN: Show only seen (not caught) Pokemon ──────────
130
+ if (filter === "seen") {
131
+ const seenOnly: { id: number; name: string; type: string }[] = [];
132
+ for (const [idStr, entry] of Object.entries(state.pokedex.entries)) {
133
+ if (entry.seen && !entry.caught) {
134
+ const pokemonId = Number(idStr);
135
+ const species = POKEMON_BY_ID.get(pokemonId);
136
+ if (species) {
137
+ seenOnly.push({
138
+ id: pokemonId,
139
+ name: species.name,
140
+ type: formatTypes(species.types),
141
+ });
142
+ }
143
+ }
144
+ }
145
+
146
+ if (seenOnly.length === 0) {
147
+ return {
148
+ content: [
149
+ {
150
+ type: "text" as const,
151
+ text: "No seen-but-uncaught Pokemon. Either you've caught them all or haven't encountered any!",
152
+ },
153
+ ],
154
+ };
155
+ }
156
+
157
+ const lines: string[] = [];
158
+ lines.push(`SEEN POKEMON (not yet caught: ${seenOnly.length})`);
159
+ lines.push("");
160
+
161
+ for (const entry of seenOnly) {
162
+ const num = String(entry.id).padStart(3, "0");
163
+ lines.push(`#${num} ${entry.name.padEnd(12)} ${entry.type}`);
164
+ }
165
+
166
+ return {
167
+ content: [{ type: "text" as const, text: lines.join("\n") }],
168
+ };
169
+ }
170
+
171
+ return {
172
+ content: [{ type: "text" as const, text: "Unknown filter. Use all, caught, or seen." }],
173
+ isError: true,
174
+ };
175
+ },
176
+ );
177
+ }
178
+
179
+ /** Handle detailed view for a specific Pokemon by name or number. */
180
+ function handlePokemonDetail(
181
+ query: string,
182
+ state: import("../../engine/types.js").PlayerState,
183
+ _stateManager: StateManager,
184
+ ): { content: { type: "text"; text: string }[] } {
185
+ // Find by number or name
186
+ let species: import("../../engine/types.js").Pokemon | undefined;
187
+
188
+ const asNumber = parseInt(query, 10);
189
+ if (!isNaN(asNumber) && asNumber >= 1 && asNumber <= TOTAL_POKEMON) {
190
+ species = POKEMON_BY_ID.get(asNumber);
191
+ }
192
+
193
+ if (!species) {
194
+ const lowerQuery = query.toLowerCase();
195
+ species = POKEDEX.find((p) => p.name.toLowerCase() === lowerQuery);
196
+ }
197
+
198
+ if (!species) {
199
+ return {
200
+ content: [
201
+ {
202
+ type: "text" as const,
203
+ text: `Pokemon "${query}" not found. Try a name (e.g., "Pikachu") or number (1-151).`,
204
+ },
205
+ ],
206
+ };
207
+ }
208
+
209
+ const entry = state.pokedex.entries[species.id];
210
+ const caught = entry?.caught ?? false;
211
+ const seen = entry?.seen ?? false;
212
+
213
+ const typeStr = formatTypes(species.types);
214
+ const num = String(species.id).padStart(3, "0");
215
+
216
+ const W = 42;
217
+ const border = "\u2500".repeat(W);
218
+ const lines: string[] = [];
219
+
220
+ lines.push(`\u250c${border}\u2510`);
221
+ lines.push(`\u2502 ${pad(`#${num} ${species.name}`, W - 2)}\u2502`);
222
+ lines.push(`\u2502 ${pad(`Type: ${typeStr}`, W - 2)}\u2502`);
223
+ lines.push(`\u2502 ${pad(`Rarity: ${species.rarity}`, W - 2)}\u2502`);
224
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
225
+
226
+ // Status
227
+ const statusStr = caught ? "\u25cf Caught" : seen ? "\u25d0 Seen" : "\u25cb Unknown";
228
+ lines.push(`\u2502 ${pad(`Status: ${statusStr}`, W - 2)}\u2502`);
229
+
230
+ if (entry?.firstSeen) {
231
+ lines.push(`\u2502 ${pad(`First seen: ${entry.firstSeen.slice(0, 10)}`, W - 2)}\u2502`);
232
+ }
233
+ if (entry?.firstCaught) {
234
+ lines.push(`\u2502 ${pad(`First caught: ${entry.firstCaught.slice(0, 10)}`, W - 2)}\u2502`);
235
+ }
236
+
237
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
238
+ lines.push(`\u2502 ${pad(`"${species.description}"`, W - 2)}\u2502`);
239
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
240
+
241
+ // Base stats
242
+ lines.push(`\u2502 ${pad("Base Stats:", W - 2)}\u2502`);
243
+ for (const stat of CODING_STATS) {
244
+ const statKey = stat as CodingStat;
245
+ const baseStatKey = CODING_TO_BASE[statKey];
246
+ const value = Math.floor(species.baseStats[baseStatKey] * 0.5);
247
+ const bar = renderStatBar(value);
248
+ const label = STAT_DISPLAY_NAMES[statKey].padEnd(10);
249
+ lines.push(`\u2502 ${pad(`${label} ${bar} ${String(value).padStart(3)}`, W - 2)}\u2502`);
250
+ }
251
+
252
+ // Evolution chain
253
+ const chain = findEvolutionChain(species.id);
254
+ if (chain && chain.links.length > 0) {
255
+ lines.push(`\u2502${" ".repeat(W)}\u2502`);
256
+ lines.push(`\u2502 ${pad("Evolution Chain:", W - 2)}\u2502`);
257
+
258
+ // Collect all unique IDs in chain order
259
+ const chainIds: number[] = [];
260
+ const visited = new Set<number>();
261
+ for (const link of chain.links) {
262
+ if (!visited.has(link.from)) {
263
+ chainIds.push(link.from);
264
+ visited.add(link.from);
265
+ }
266
+ if (!visited.has(link.to)) {
267
+ chainIds.push(link.to);
268
+ visited.add(link.to);
269
+ }
270
+ }
271
+
272
+ const chainNames = chainIds.map((id) => {
273
+ const sp = POKEMON_BY_ID.get(id);
274
+ const name = sp ? sp.name : `#${id}`;
275
+ const isCurrent = id === species!.id;
276
+ return isCurrent ? `[${name}]` : name;
277
+ });
278
+ lines.push(`\u2502 ${pad(chainNames.join(" \u2192 "), W - 2)}\u2502`);
279
+ }
280
+
281
+ lines.push(`\u2514${border}\u2518`);
282
+
283
+ return {
284
+ content: [{ type: "text" as const, text: lines.join("\n") }],
285
+ };
286
+ }