@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,308 @@
1
+ /**
2
+ * Wild encounter system.
3
+ * Pokemon appear based on coding activity type and are catchable
4
+ * based on the active Pokemon's stats and level.
5
+ */
6
+
7
+ import type {
8
+ XpEventType,
9
+ PokemonType,
10
+ PlayerState,
11
+ WildEncounter,
12
+ OwnedPokemon,
13
+ CatchCondition,
14
+ CodingStat,
15
+ RarityTier,
16
+ } from "./types.js";
17
+ import { XP_PER_ENCOUNTER } from "./constants.js";
18
+ import { POKEMON_BY_ID } from "./pokemon-data.js";
19
+ import { TYPE_POOLS } from "./encounter-pool.js";
20
+
21
+ // ── Activity to Pokemon Type Mapping ──────────────────────────
22
+
23
+ const ENCOUNTER_TYPE_MAP: Readonly<Record<XpEventType, readonly PokemonType[]>> = {
24
+ commit: ["Normal", "Flying"],
25
+ test_pass: ["Fighting", "Normal"],
26
+ test_written: ["Fighting", "Normal"],
27
+ build_success: ["Fire", "Rock"],
28
+ bug_fix: ["Bug", "Poison"],
29
+ lint_fix: ["Bug", "Poison"],
30
+ file_create: ["Normal", "Ground"],
31
+ file_edit: ["Normal", "Ground"],
32
+ search: ["Flying", "Ground"],
33
+ large_refactor: ["Psychic", "Dragon"],
34
+ session_start: ["Grass", "Normal"],
35
+ daily_streak: ["Water", "Electric"],
36
+ pet: ["Normal"],
37
+ };
38
+
39
+ /** Maps an XP event type to the Pokemon types that can appear. */
40
+ export function getEncounterTypes(eventType: XpEventType): readonly PokemonType[] {
41
+ return ENCOUNTER_TYPE_MAP[eventType];
42
+ }
43
+
44
+ // ── Encounter Trigger ─────────────────────────────────────────
45
+
46
+ /**
47
+ * Check if a wild encounter should trigger based on XP earned since last encounter.
48
+ * Returns true roughly every XP_PER_ENCOUNTER (500) XP.
49
+ */
50
+ export function shouldTriggerEncounter(xpSinceLastEncounter: number): boolean {
51
+ return xpSinceLastEncounter >= XP_PER_ENCOUNTER;
52
+ }
53
+
54
+ // ── Rarity Weights ────────────────────────────────────────────
55
+
56
+ /** Relative weights for rarity-based selection. Higher = more likely to appear. */
57
+ const RARITY_WEIGHTS: Readonly<Record<"common" | "uncommon" | "rare", number>> = {
58
+ common: 70,
59
+ uncommon: 25,
60
+ rare: 5,
61
+ };
62
+
63
+ // ── Catch Condition Mapping ───────────────────────────────────
64
+
65
+ /**
66
+ * Stat most relevant to each Pokemon type for catch condition evaluation.
67
+ * Used for uncommon+ encounters to determine the required stat.
68
+ */
69
+ const TYPE_TO_STAT: Readonly<Record<PokemonType, CodingStat>> = {
70
+ Normal: "velocity",
71
+ Fire: "debugging",
72
+ Water: "stability",
73
+ Electric: "velocity",
74
+ Grass: "stamina",
75
+ Ice: "stability",
76
+ Fighting: "debugging",
77
+ Poison: "debugging",
78
+ Ground: "stability",
79
+ Flying: "velocity",
80
+ Psychic: "wisdom",
81
+ Bug: "debugging",
82
+ Rock: "stability",
83
+ Ghost: "wisdom",
84
+ Dragon: "wisdom",
85
+ };
86
+
87
+ /** Minimum stat thresholds by rarity tier. */
88
+ const RARITY_STAT_THRESHOLD: Readonly<Record<RarityTier, number>> = {
89
+ common: 0,
90
+ uncommon: 20,
91
+ rare: 40,
92
+ legendary: 60,
93
+ mythical: 80,
94
+ };
95
+
96
+ /** Minimum Pokemon level required to catch by rarity tier. */
97
+ const RARITY_LEVEL_THRESHOLD: Readonly<Record<RarityTier, number>> = {
98
+ common: 1,
99
+ uncommon: 10,
100
+ rare: 25,
101
+ legendary: 50,
102
+ mythical: 75,
103
+ };
104
+
105
+ // ── Catch Condition ───────────────────────────────────────────
106
+
107
+ /** Get the catch condition for a Pokemon based on its rarity. */
108
+ export function getCatchCondition(pokemonId: number): CatchCondition {
109
+ const pokemon = POKEMON_BY_ID.get(pokemonId);
110
+ if (!pokemon) {
111
+ return { requiredStat: null, minStatValue: 0, requiredLevel: 1 };
112
+ }
113
+
114
+ const rarity = pokemon.rarity;
115
+ const requiredLevel = RARITY_LEVEL_THRESHOLD[rarity];
116
+ const minStatValue = RARITY_STAT_THRESHOLD[rarity];
117
+
118
+ // Common Pokemon have no stat requirement
119
+ if (rarity === "common") {
120
+ return { requiredStat: null, minStatValue: 0, requiredLevel };
121
+ }
122
+
123
+ // Use the primary type to determine which stat is required
124
+ const primaryType = pokemon.types[0];
125
+ const requiredStat = TYPE_TO_STAT[primaryType];
126
+
127
+ return { requiredStat, minStatValue, requiredLevel };
128
+ }
129
+
130
+ // ── Encounter Generation ──────────────────────────────────────
131
+
132
+ /**
133
+ * Collect all candidate Pokemon IDs for a set of types, respecting
134
+ * ownership rules and rarity constraints.
135
+ */
136
+ function buildCandidatePool(
137
+ types: readonly PokemonType[],
138
+ state: PlayerState,
139
+ ): { id: number; weight: number }[] {
140
+ const candidates: { id: number; weight: number }[] = [];
141
+ const seen = new Set<number>();
142
+
143
+ // Determine the player's starter Pokemon ID
144
+ const starterPokemon = [...state.party, ...state.pcBox].find((p) => p.isStarter);
145
+ const starterPokemonId = starterPokemon?.pokemonId ?? -1;
146
+
147
+ // Set of already-caught Pokemon IDs (for duplicate filtering)
148
+ const caughtIds = new Set<number>();
149
+ for (const [idStr, entry] of Object.entries(state.pokedex.entries)) {
150
+ if (entry.caught) {
151
+ caughtIds.add(Number(idStr));
152
+ }
153
+ }
154
+
155
+ for (const pokemonType of types) {
156
+ const pool = TYPE_POOLS.get(pokemonType);
157
+ if (!pool) continue;
158
+
159
+ for (const rarity of ["common", "uncommon", "rare"] as const) {
160
+ const ids = pool[rarity];
161
+ const weight = RARITY_WEIGHTS[rarity];
162
+
163
+ for (const id of ids) {
164
+ // Skip if already added from another type overlap
165
+ if (seen.has(id)) continue;
166
+ seen.add(id);
167
+
168
+ // Exclude the player's starter from wild encounters
169
+ if (id === starterPokemonId) continue;
170
+
171
+ // Common Pokemon: always available, duplicates allowed
172
+ // Uncommon+: skip if already caught
173
+ if (rarity !== "common" && caughtIds.has(id)) continue;
174
+
175
+ candidates.push({ id, weight });
176
+ }
177
+ }
178
+ }
179
+
180
+ return candidates;
181
+ }
182
+
183
+ /**
184
+ * Deterministic pseudo-random number generator seeded by the current second.
185
+ * Uses a simple mulberry32 approach for reproducibility within the same second.
186
+ */
187
+ function seededRandom(seed: number): number {
188
+ let t = (seed + 0x6d2b79f5) | 0;
189
+ t = Math.imul(t ^ (t >>> 15), t | 1);
190
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
191
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
192
+ }
193
+
194
+ /**
195
+ * Select a Pokemon ID from the weighted candidate pool using a deterministic seed.
196
+ * Returns null if the pool is empty.
197
+ */
198
+ function weightedSelect(
199
+ candidates: readonly { id: number; weight: number }[],
200
+ seed: number,
201
+ ): number | null {
202
+ if (candidates.length === 0) return null;
203
+
204
+ const totalWeight = candidates.reduce((sum, c) => sum + c.weight, 0);
205
+ const roll = seededRandom(seed) * totalWeight;
206
+
207
+ let cumulative = 0;
208
+ for (const candidate of candidates) {
209
+ cumulative += candidate.weight;
210
+ if (roll < cumulative) {
211
+ return candidate.id;
212
+ }
213
+ }
214
+
215
+ // Fallback to last candidate (floating-point edge case)
216
+ return candidates[candidates.length - 1]!.id;
217
+ }
218
+
219
+ /**
220
+ * Determine the level of a wild encounter Pokemon.
221
+ * Scales with the player's strongest Pokemon level, with some variance.
222
+ */
223
+ function determineEncounterLevel(state: PlayerState, seed: number): number {
224
+ const allPokemon = [...state.party, ...state.pcBox];
225
+ const maxLevel = allPokemon.reduce((max, p) => Math.max(max, p.level), 1);
226
+
227
+ // Wild Pokemon appear at 60-100% of the player's max level, minimum 2
228
+ const minLevel = Math.max(2, Math.floor(maxLevel * 0.6));
229
+ const range = Math.max(1, maxLevel - minLevel + 1);
230
+ const level = minLevel + Math.floor(seededRandom(seed + 7) * range);
231
+
232
+ return Math.min(level, 100);
233
+ }
234
+
235
+ /**
236
+ * Generate a wild encounter based on the activity type.
237
+ * Picks a Pokemon from the matching type pool, weighted by rarity.
238
+ * Excludes Pokemon already in the player's party/box (unless duplicates are common tier).
239
+ * Returns null if no eligible Pokemon found.
240
+ */
241
+ export function generateEncounter(
242
+ eventType: XpEventType,
243
+ state: PlayerState,
244
+ ): WildEncounter | null {
245
+ const types = getEncounterTypes(eventType);
246
+ const candidates = buildCandidatePool(types, state);
247
+
248
+ if (candidates.length === 0) return null;
249
+
250
+ const seed = Math.floor(Date.now() / 1000);
251
+ const pokemonId = weightedSelect(candidates, seed);
252
+
253
+ if (pokemonId === null) return null;
254
+
255
+ const level = determineEncounterLevel(state, seed);
256
+ const catchCondition = getCatchCondition(pokemonId);
257
+
258
+ return { pokemonId, level, catchCondition };
259
+ }
260
+
261
+ // ── Catch Evaluation ──────────────────────────────────────────
262
+
263
+ /**
264
+ * Check if the active Pokemon can catch the encountered Pokemon.
265
+ * Based on catch rate, required stats, and level.
266
+ */
267
+ export function canCatch(
268
+ encounter: WildEncounter,
269
+ activePokemon: OwnedPokemon,
270
+ ): { success: boolean; reason: string } {
271
+ const pokemon = POKEMON_BY_ID.get(encounter.pokemonId);
272
+ if (!pokemon) {
273
+ return { success: false, reason: "Unknown Pokemon" };
274
+ }
275
+
276
+ const { requiredStat, minStatValue, requiredLevel } = encounter.catchCondition;
277
+
278
+ // Check level requirement
279
+ if (activePokemon.level < requiredLevel) {
280
+ return {
281
+ success: false,
282
+ reason: `Need level ${requiredLevel} to catch ${pokemon.rarity} Pokemon (currently level ${activePokemon.level})`,
283
+ };
284
+ }
285
+
286
+ // Check stat requirement
287
+ if (requiredStat !== null) {
288
+ const currentStat = activePokemon.codingStats[requiredStat];
289
+ if (currentStat < minStatValue) {
290
+ const primaryType = pokemon.types[0];
291
+ return {
292
+ success: false,
293
+ reason: `Need ${requiredStat.toUpperCase()} at ${minStatValue} to catch ${primaryType} types (currently ${currentStat})`,
294
+ };
295
+ }
296
+ }
297
+
298
+ // Requirements met — roll against catch rate
299
+ const seed = Math.floor(Date.now() / 1000);
300
+ const roll = seededRandom(seed + encounter.pokemonId) * 255;
301
+ const success = pokemon.catchRate > roll;
302
+
303
+ if (success) {
304
+ return { success: true, reason: `Caught ${pokemon.name}!` };
305
+ }
306
+
307
+ return { success: false, reason: `${pokemon.name} broke free!` };
308
+ }