getcompetitive 1.1.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.
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/archetypes.js +712 -0
- package/dist/archetypes.js.map +1 -0
- package/dist/dex.js +414 -0
- package/dist/dex.js.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/regulations.data.js +854 -0
- package/dist/regulations.data.js.map +1 -0
- package/dist/regulations.js +91 -0
- package/dist/regulations.js.map +1 -0
- package/dist/result.js +50 -0
- package/dist/result.js.map +1 -0
- package/dist/threats.js +336 -0
- package/dist/threats.js.map +1 -0
- package/dist/tools/analyze.js +268 -0
- package/dist/tools/analyze.js.map +1 -0
- package/dist/tools/calc.js +718 -0
- package/dist/tools/calc.js.map +1 -0
- package/dist/tools/data.js +763 -0
- package/dist/tools/data.js.map +1 -0
- package/dist/tools/meta.js +158 -0
- package/dist/tools/meta.js.map +1 -0
- package/dist/tools/regulations.js +332 -0
- package/dist/tools/regulations.js.map +1 -0
- package/dist/tools/schemas.js +13 -0
- package/dist/tools/schemas.js.map +1 -0
- package/dist/tools/team.js +221 -0
- package/dist/tools/team.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pokemon data tools: species, forms, moves, items, abilities, natures,
|
|
3
|
+
* learnsets, types, and type effectiveness.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { getDex, normalizeGen, toID, speciesToObj, moveToObj, itemToObj, abilityToObj, natureToObj, typeToObj, learnsetToObj, typeEffectiveness, TYPES18, } from '../dex.js';
|
|
7
|
+
import { ok, wrap, requireExists, READ_ONLY_ANNOTATIONS } from '../result.js';
|
|
8
|
+
import { genSchema } from './schemas.js';
|
|
9
|
+
/**
|
|
10
|
+
* Output-schema fragments. Every lookup below returns a projection built in
|
|
11
|
+
* `src/dex.ts` (`speciesToObj`, `moveToObj`, …), so the shapes those helpers
|
|
12
|
+
* produce are declared once here and reused, descriptions included.
|
|
13
|
+
*/
|
|
14
|
+
/** The six stats of a species, keyed by Showdown stat id. */
|
|
15
|
+
const statsTable = z
|
|
16
|
+
.object({
|
|
17
|
+
hp: z.number().describe('Hit Points.'),
|
|
18
|
+
atk: z.number().describe('Attack.'),
|
|
19
|
+
def: z.number().describe('Defense.'),
|
|
20
|
+
spa: z.number().describe('Special Attack.'),
|
|
21
|
+
spd: z.number().describe('Special Defense.'),
|
|
22
|
+
spe: z.number().describe('Speed.'),
|
|
23
|
+
})
|
|
24
|
+
.describe('All six stats keyed by stat id ("hp", "atk", "def", "spa", "spd", "spe").');
|
|
25
|
+
/** Stat stages, as produced by moves, Z-moves, and held items. */
|
|
26
|
+
const boostsTable = z
|
|
27
|
+
.record(z.string(), z.number())
|
|
28
|
+
.describe('Stat changes keyed by stat id, e.g. {"atk": -1} for one stage of Attack drop. Keys are "atk", "def", "spa", "spd", "spe", and (for secondary effects) "accuracy" or "evasion"; positive values raise the stat, negative values lower it.');
|
|
29
|
+
/**
|
|
30
|
+
* Showdown stores flags as a sparse map of `{flag: 1}` — a flag that does not
|
|
31
|
+
* apply is simply absent, never 0.
|
|
32
|
+
*/
|
|
33
|
+
const moveFlags = z
|
|
34
|
+
.record(z.string(), z.number())
|
|
35
|
+
.describe('Flags the move carries, as {"<flag>": 1}; any flag not listed does not apply. Possible keys: "allyanim", "bite", "bullet", "bypasssub", "cantusetwice", "charge", "contact", "dance", "defrost", "distance", "failcopycat", "failencore", "failinstruct", "failmefirst", "failmimic", "futuremove", "gravity", "heal", "metronome", "minimize", "mirror", "mustpressure", "noassist", "nonsky", "noparentalbond", "nosketch", "nosleeptalk", "pledgecombo", "powder", "protect", "pulse", "punch", "recharge", "reflectable", "slicing", "snatch", "sound", "wind".');
|
|
36
|
+
const abilityFlags = z
|
|
37
|
+
.record(z.string(), z.number())
|
|
38
|
+
.describe('Flags the ability carries, as {"<flag>": 1}; any flag not listed does not apply. Possible keys: "breakable", "cantsuppress", "failroleplay", "failskillswap", "noentrain", "noreceiver", "notrace", "notransform".');
|
|
39
|
+
/** The ability slots of a species. */
|
|
40
|
+
const abilitySlots = z
|
|
41
|
+
.object({
|
|
42
|
+
'0': z.string().describe('Primary ability.'),
|
|
43
|
+
'1': z
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('Secondary ability; absent from species with only one normal ability.'),
|
|
47
|
+
H: z.string().optional().describe('Hidden ability; absent from species that have none.'),
|
|
48
|
+
S: z
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe('Special slot, used by abilities that come with a forme (Battle Bond, Power Construct); absent for species that have none.'),
|
|
52
|
+
})
|
|
53
|
+
.describe('Abilities keyed by Showdown slot: "0" primary, "1" secondary, "H" hidden, "S" special.');
|
|
54
|
+
/** One effect a move's secondary hit can apply. */
|
|
55
|
+
const secondaryEffect = z
|
|
56
|
+
.object({
|
|
57
|
+
chance: z.number().optional().describe('Percent chance the effect triggers; absent when it is guaranteed.'),
|
|
58
|
+
boosts: boostsTable
|
|
59
|
+
.optional()
|
|
60
|
+
.describe('Stat changes applied to the target, e.g. {"spd": -1} for a Special Defense drop.'),
|
|
61
|
+
self: z
|
|
62
|
+
.object({
|
|
63
|
+
boosts: boostsTable.optional().describe('Stat changes applied to the user of the move.'),
|
|
64
|
+
})
|
|
65
|
+
.optional()
|
|
66
|
+
.describe('Effect applied to the user instead of the target, e.g. a self-boost or self-drop.'),
|
|
67
|
+
status: z.string().optional().describe('Major status inflicted, e.g. "brn", "par", "frz".'),
|
|
68
|
+
volatileStatus: z.string().optional().describe('Volatile status inflicted, e.g. "flinch", "confusion".'),
|
|
69
|
+
})
|
|
70
|
+
.describe('One secondary effect of the move; only the fields that apply to that effect are present.');
|
|
71
|
+
/** One attacking type's multiplier in a coverage or damage-taken map. */
|
|
72
|
+
const effectivenessEntry = z
|
|
73
|
+
.object({
|
|
74
|
+
effectiveness: z
|
|
75
|
+
.number()
|
|
76
|
+
.describe('Damage multiplier: 0 (immune), 0.25, 0.5 (resisted), 1 (neutral), 2 or 4 (weak).'),
|
|
77
|
+
label: z
|
|
78
|
+
.string()
|
|
79
|
+
.describe('The multiplier in words: "immune", "0.5x not very effective", "neutral", "2x super effective".'),
|
|
80
|
+
})
|
|
81
|
+
.describe('How one attacking type lands against the defender.');
|
|
82
|
+
const TYPE_NAMES = '"Bug", "Dark", "Dragon", "Electric", "Fairy", "Fighting", "Fire", "Flying", "Ghost", "Grass", "Ground", "Ice", "Normal", "Poison", "Psychic", "Rock", "Steel", "Water"';
|
|
83
|
+
function fuzzyMatches(ids, names, query) {
|
|
84
|
+
const q = query.toLowerCase().trim();
|
|
85
|
+
return names.filter((n, i) => n.toLowerCase().includes(q) || ids[i].includes(q));
|
|
86
|
+
}
|
|
87
|
+
function requireSpecies(dex, name) {
|
|
88
|
+
const s = dex.species.get(name);
|
|
89
|
+
const suggestions = s.exists
|
|
90
|
+
? []
|
|
91
|
+
: fuzzyMatches(dex.species.all().map((x) => x.id), dex.species.all().map((x) => x.name), name);
|
|
92
|
+
return requireExists(s, 'Pokemon species', name, suggestions);
|
|
93
|
+
}
|
|
94
|
+
export function registerDataTools(server) {
|
|
95
|
+
server.registerTool('get_pokemon', {
|
|
96
|
+
title: 'Get Pokémon data',
|
|
97
|
+
description: 'Look up one Pokémon and return its competitive profile: types, base stats and BST, abilities by slot, singles/doubles tier, weight, gender ratio, egg groups, and evolutions. Accepts any Showdown name or form, case- and punctuation-insensitive ("garchomp", "Ogerpon-Wellspring", "rotom wash"); unknown names return an isError listing near matches. Use `search` when you only have a partial name, `list_forms` for alternate or cosmetic forms, and `calculate_stats` when you need stats computed from EVs, IVs, and nature. Read-only and offline over the bundled Showdown dataset — no network, auth, or rate limits.',
|
|
98
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
99
|
+
inputSchema: {
|
|
100
|
+
species: z
|
|
101
|
+
.string()
|
|
102
|
+
.describe('Species or form name, e.g. "Garchomp", "Ogerpon-Wellspring", "Rotom-Wash".'),
|
|
103
|
+
generation: genSchema,
|
|
104
|
+
},
|
|
105
|
+
outputSchema: {
|
|
106
|
+
name: z.string().describe('Display name of the entry, e.g. "Garchomp" or "Ogerpon-Wellspring".'),
|
|
107
|
+
num: z.number().describe('National Dex number; 0 for entries that have none.'),
|
|
108
|
+
gen: z.number().describe('Generation this entry was introduced in; 0 for entries outside the numbered generations (e.g. MissingNo.).'),
|
|
109
|
+
types: z
|
|
110
|
+
.array(z.string())
|
|
111
|
+
.describe(`Typing in the requested generation, in order, e.g. ["Dragon", "Ground"]. One of ${TYPE_NAMES}.`),
|
|
112
|
+
baseStats: statsTable,
|
|
113
|
+
bst: z.number().describe('Base stat total — the sum of `baseStats`.'),
|
|
114
|
+
abilities: abilitySlots,
|
|
115
|
+
tier: z
|
|
116
|
+
.string()
|
|
117
|
+
.describe('Singles tier label from the bundled dataset, e.g. "OU", "UU", "LC".'),
|
|
118
|
+
doublesTier: z.string().describe('Doubles (VGC) tier label from the bundled dataset.'),
|
|
119
|
+
natDexTier: z.string().describe('National Dex tier label; the empty string when the entry has none.'),
|
|
120
|
+
baseSpecies: z.string().describe('Name of the species this entry belongs to; equal to `name` for a base forme.'),
|
|
121
|
+
forme: z.string().optional().describe('Forme label when this entry is an alternate forme, e.g. "Mega", "Wash"; absent for the base forme.'),
|
|
122
|
+
baseForme: z.string().optional().describe('Label of the species\' default forme when it has formes, e.g. "Teal" for Ogerpon; absent otherwise.'),
|
|
123
|
+
otherFormes: z
|
|
124
|
+
.array(z.string())
|
|
125
|
+
.nullable()
|
|
126
|
+
.optional()
|
|
127
|
+
.describe('Names of the non-cosmetic alternate formes; null or absent when the species has none.'),
|
|
128
|
+
cosmeticFormes: z
|
|
129
|
+
.array(z.string())
|
|
130
|
+
.nullable()
|
|
131
|
+
.optional()
|
|
132
|
+
.describe('Names of cosmetic-only formes (different look, identical mechanics); null or absent when there are none.'),
|
|
133
|
+
formeOrder: z
|
|
134
|
+
.array(z.string())
|
|
135
|
+
.nullable()
|
|
136
|
+
.optional()
|
|
137
|
+
.describe('Dataset display order of the species and its formes; absent when the species has no formes.'),
|
|
138
|
+
isCosmeticForme: z.boolean().describe('true when this entry is a cosmetic forme rather than a mechanically distinct one.'),
|
|
139
|
+
battleOnly: z
|
|
140
|
+
.union([z.string(), z.array(z.string())])
|
|
141
|
+
.optional()
|
|
142
|
+
.describe('The species this entry transforms from during battle, e.g. "Garchomp" for Garchomp-Mega, or a list when it can come from more than one (Wishiwashi); absent for normally selectable entries.'),
|
|
143
|
+
weightkg: z.number().describe('Weight in kilograms, as used by weight-based moves such as Heavy Slam.'),
|
|
144
|
+
genderRatio: z
|
|
145
|
+
.object({
|
|
146
|
+
M: z.number().describe('Fraction of encounters that are male.'),
|
|
147
|
+
F: z.number().describe('Fraction of encounters that are female.'),
|
|
148
|
+
})
|
|
149
|
+
.describe('Gender odds at encounter; both 0 for genderless species.'),
|
|
150
|
+
gender: z
|
|
151
|
+
.string()
|
|
152
|
+
.optional()
|
|
153
|
+
.describe('"M", "F", or "N" (genderless) for species that are not a mix of both; absent when the species has a mixed gender ratio.'),
|
|
154
|
+
eggGroups: z.array(z.string()).describe('Egg groups, e.g. ["Monster", "Dragon"]; empty for species that cannot breed.'),
|
|
155
|
+
nfe: z.boolean().describe('true when the species can still evolve (not fully evolved).'),
|
|
156
|
+
canHatch: z.boolean().describe('true when the species can hatch from an Egg.'),
|
|
157
|
+
prevo: z.string().optional().describe('Species this one evolves from; absent for base evolutions.'),
|
|
158
|
+
evos: z.array(z.string()).describe('Species this one evolves into; empty when it does not evolve.'),
|
|
159
|
+
evoLevel: z.number().optional().describe('Level required to evolve, when the evolution is level-based.'),
|
|
160
|
+
evoItem: z.string().optional().describe('Item required to evolve, when the evolution is item-based.'),
|
|
161
|
+
evoMove: z.string().optional().describe('Move the species must know to evolve, when the evolution is move-based.'),
|
|
162
|
+
evoCondition: z
|
|
163
|
+
.string()
|
|
164
|
+
.optional()
|
|
165
|
+
.describe('Free-text condition for evolutions that are not plain level, item, or move evolutions, e.g. "Level up with 999 Coins in the bag".'),
|
|
166
|
+
isMega: z.boolean().optional().describe('true for Mega Evolutions; absent otherwise.'),
|
|
167
|
+
isPrimal: z.boolean().optional().describe('true for Primal Reversions; absent otherwise.'),
|
|
168
|
+
canGigantamax: z
|
|
169
|
+
.string()
|
|
170
|
+
.optional()
|
|
171
|
+
.describe('Name of the G-Max move, when this entry is a Gigantamax-capable forme; absent otherwise.'),
|
|
172
|
+
cannotDynamax: z.boolean().describe('true when the species cannot Dynamax.'),
|
|
173
|
+
requiredTeraType: z
|
|
174
|
+
.string()
|
|
175
|
+
.optional()
|
|
176
|
+
.describe('Tera type this entry is locked to (e.g. an Ogerpon mask); absent when the Tera type is freely chosen.'),
|
|
177
|
+
isNonstandard: z
|
|
178
|
+
.string()
|
|
179
|
+
.nullable()
|
|
180
|
+
.describe('"Past", "Future", "Unobtainable", or "CAP" when the entry is not available in the current games; null when it is standard.'),
|
|
181
|
+
unreleasedHidden: z.boolean().optional().describe('true when the hidden ability has not been released; absent otherwise.'),
|
|
182
|
+
tags: z
|
|
183
|
+
.array(z.string())
|
|
184
|
+
.describe('Dataset tags such as ["Sub-Legendary"] or ["Mythical"]; empty when the entry is untagged.'),
|
|
185
|
+
},
|
|
186
|
+
}, wrap(async (args) => {
|
|
187
|
+
const gen = normalizeGen(args.generation);
|
|
188
|
+
const s = requireSpecies(getDex(gen), args.species);
|
|
189
|
+
return ok(speciesToObj(s));
|
|
190
|
+
}));
|
|
191
|
+
server.registerTool('list_forms', {
|
|
192
|
+
title: 'List Pokémon forms',
|
|
193
|
+
description: 'List every form of one species — base, alternate, cosmetic, and battle-only — with each form\'s types, base stats, abilities, and tier, plus the total count. Forms that do not exist in the requested generation are returned with a note rather than dropped, so a missing entry is visible. Use it before assuming a form exists; for a single species\' full profile use `get_pokemon`, and to search names across species use `search`. Read-only and offline; unknown species return an isError with near matches.',
|
|
194
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
195
|
+
inputSchema: {
|
|
196
|
+
species: z
|
|
197
|
+
.string()
|
|
198
|
+
.describe('Base species to expand, e.g. "Rotom", "Ogerpon", "Gholdengo".'),
|
|
199
|
+
generation: genSchema,
|
|
200
|
+
},
|
|
201
|
+
outputSchema: {
|
|
202
|
+
baseSpecies: z
|
|
203
|
+
.string()
|
|
204
|
+
.describe('Name of the base species whose forms are listed, e.g. "Rotom" when asked for "Rotom-Wash".'),
|
|
205
|
+
count: z.number().describe('Number of entries in `forms`, including placeholders for forms missing in this generation.'),
|
|
206
|
+
forms: z
|
|
207
|
+
.array(z.union([
|
|
208
|
+
z
|
|
209
|
+
.object({
|
|
210
|
+
name: z.string().describe('Form name, e.g. "Rotom-Wash".'),
|
|
211
|
+
types: z.array(z.string()).describe(`Typing of the form, e.g. ["Electric", "Water"]. One of ${TYPE_NAMES}.`),
|
|
212
|
+
baseStats: statsTable,
|
|
213
|
+
bst: z.number().describe('Base stat total of the form.'),
|
|
214
|
+
abilities: abilitySlots,
|
|
215
|
+
tier: z.string().describe('Singles tier label of the form; may be empty for untiered forms.'),
|
|
216
|
+
doublesTier: z.string().describe('Doubles (VGC) tier label of the form; may be empty.'),
|
|
217
|
+
isCosmetic: z.boolean().describe('true when the form differs only cosmetically.'),
|
|
218
|
+
battleOnly: z
|
|
219
|
+
.union([z.string(), z.array(z.string())])
|
|
220
|
+
.optional()
|
|
221
|
+
.describe('The species this form transforms from in battle; absent for normally selectable forms.'),
|
|
222
|
+
isMega: z.boolean().optional().describe('true for Mega Evolutions; absent otherwise.'),
|
|
223
|
+
isPrimal: z.boolean().optional().describe('true for Primal Reversions; absent otherwise.'),
|
|
224
|
+
})
|
|
225
|
+
.describe('A form that exists in the requested generation, carrying the same fields as `get_pokemon` (minus evolutions and the long tail of dataset metadata).'),
|
|
226
|
+
z
|
|
227
|
+
.object({
|
|
228
|
+
name: z.string().describe('Form name that has no data in the requested generation.'),
|
|
229
|
+
note: z.string().describe('Why the entry is empty — currently always "unavailable in this generation".'),
|
|
230
|
+
})
|
|
231
|
+
.describe('Placeholder for a form name the dataset knows about but that does not exist in the requested generation; distinguishes "no such form" from "form missing here".'),
|
|
232
|
+
]))
|
|
233
|
+
.describe('One entry per known form name of the species — base, alternate, cosmetic, and battle-only — either a full form profile or a `{name, note}` placeholder.'),
|
|
234
|
+
},
|
|
235
|
+
}, wrap(async (args) => {
|
|
236
|
+
const gen = normalizeGen(args.generation);
|
|
237
|
+
const dex = getDex(gen);
|
|
238
|
+
const base = requireSpecies(dex, args.species);
|
|
239
|
+
const names = new Set([base.name]);
|
|
240
|
+
for (const f of [...(base.otherFormes ?? []), ...(base.cosmeticFormes ?? []), ...(base.formeOrder ?? [])]) {
|
|
241
|
+
if (f)
|
|
242
|
+
names.add(f);
|
|
243
|
+
}
|
|
244
|
+
const forms = [...names].map((n) => {
|
|
245
|
+
const f = dex.species.get(n);
|
|
246
|
+
if (!f.exists)
|
|
247
|
+
return { name: n, note: 'unavailable in this generation' };
|
|
248
|
+
const o = speciesToObj(f);
|
|
249
|
+
return {
|
|
250
|
+
name: o.name,
|
|
251
|
+
types: o.types,
|
|
252
|
+
baseStats: o.baseStats,
|
|
253
|
+
bst: o.bst,
|
|
254
|
+
abilities: o.abilities,
|
|
255
|
+
tier: o.tier,
|
|
256
|
+
doublesTier: o.doublesTier,
|
|
257
|
+
isCosmetic: o.isCosmeticForme,
|
|
258
|
+
battleOnly: o.battleOnly,
|
|
259
|
+
isMega: o.isMega,
|
|
260
|
+
isPrimal: o.isPrimal,
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
return ok({
|
|
264
|
+
baseSpecies: base.baseSpecies || base.name,
|
|
265
|
+
count: forms.length,
|
|
266
|
+
forms,
|
|
267
|
+
});
|
|
268
|
+
}));
|
|
269
|
+
server.registerTool('search', {
|
|
270
|
+
title: 'Search the dataset by name',
|
|
271
|
+
description: 'Find species, moves, items, abilities, or natures by case-insensitive substring of name or Showdown id, sorted by National Dex number and truncated to `limit`. Use it when the exact name is uncertain, then call the matching lookup (`get_pokemon`, `get_move`, `get_item`, `get_ability`, `get_nature`) with the name it returns. Exactly one `kind` is searched per call; species results carry their tier, and the reply echoes kind, query, and total match count. Read-only and offline; a blank query is rejected as an error instead of dumping the dataset.',
|
|
272
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
273
|
+
inputSchema: {
|
|
274
|
+
query: z
|
|
275
|
+
.string()
|
|
276
|
+
.describe('Substring to match against name and Showdown id, e.g. "oger", "sword".'),
|
|
277
|
+
kind: z
|
|
278
|
+
.enum(['species', 'move', 'item', 'ability', 'nature'])
|
|
279
|
+
.default('species')
|
|
280
|
+
.describe('Table to search; one per call (default "species").'),
|
|
281
|
+
limit: z
|
|
282
|
+
.number()
|
|
283
|
+
.int()
|
|
284
|
+
.min(1)
|
|
285
|
+
.max(100)
|
|
286
|
+
.default(20)
|
|
287
|
+
.describe('Maximum results returned, 1-100 (default 20).'),
|
|
288
|
+
generation: genSchema,
|
|
289
|
+
},
|
|
290
|
+
outputSchema: {
|
|
291
|
+
kind: z
|
|
292
|
+
.enum(['species', 'move', 'item', 'ability', 'nature'])
|
|
293
|
+
.describe('The table that was searched, echoed back from the `kind` argument.'),
|
|
294
|
+
query: z.string().describe('The query exactly as it was supplied (not lower-cased or trimmed).'),
|
|
295
|
+
count: z
|
|
296
|
+
.number()
|
|
297
|
+
.describe('Total number of matches the query found, before truncation to `limit` — compare against `results.length` to see whether the list was cut short.'),
|
|
298
|
+
results: z
|
|
299
|
+
.array(z.object({
|
|
300
|
+
name: z.string().describe('Match name, usable as-is with the matching lookup tool, e.g. "Garchomp".'),
|
|
301
|
+
num: z.number().describe('National Dex (or table) number, the sort key for this list.'),
|
|
302
|
+
tier: z
|
|
303
|
+
.string()
|
|
304
|
+
.optional()
|
|
305
|
+
.describe('Competitive tier — present only when `kind` is "species"; absent for moves, items, abilities, and natures.'),
|
|
306
|
+
}))
|
|
307
|
+
.describe('Matching entries sorted by `num` ascending and truncated to `limit`; empty when nothing matched.'),
|
|
308
|
+
},
|
|
309
|
+
}, wrap(async (args) => {
|
|
310
|
+
const gen = normalizeGen(args.generation);
|
|
311
|
+
const dex = getDex(gen);
|
|
312
|
+
const q = args.query.toLowerCase().trim();
|
|
313
|
+
if (!q)
|
|
314
|
+
throw new Error('query must be non-empty.');
|
|
315
|
+
const results = [];
|
|
316
|
+
const push = (all) => {
|
|
317
|
+
for (const e of all) {
|
|
318
|
+
if (e.name.toLowerCase().includes(q) || e.id.includes(q)) {
|
|
319
|
+
results.push({ name: e.name, num: e.num, tier: e.tier });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
if (args.kind === 'species') {
|
|
324
|
+
push(dex.species.all().map((s) => ({ id: s.id, name: s.name, num: s.num, tier: s.tier })));
|
|
325
|
+
}
|
|
326
|
+
else if (args.kind === 'move') {
|
|
327
|
+
push(dex.moves.all().map((m) => ({ id: m.id, name: m.name, num: m.num })));
|
|
328
|
+
}
|
|
329
|
+
else if (args.kind === 'item') {
|
|
330
|
+
push(dex.items.all().map((i) => ({ id: i.id, name: i.name, num: i.num })));
|
|
331
|
+
}
|
|
332
|
+
else if (args.kind === 'ability') {
|
|
333
|
+
push(dex.abilities.all().map((a) => ({ id: a.id, name: a.name, num: a.num })));
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
push(dex.natures.all().map((n) => ({ id: n.id, name: n.name, num: n.num })));
|
|
337
|
+
}
|
|
338
|
+
results.sort((a, b) => a.num - b.num);
|
|
339
|
+
return ok({ kind: args.kind, query: args.query, count: results.length, results: results.slice(0, args.limit) });
|
|
340
|
+
}));
|
|
341
|
+
server.registerTool('get_move', {
|
|
342
|
+
title: 'Get move data',
|
|
343
|
+
description: 'Get one move\'s battle data: type, damage class, base power, accuracy, PP, priority, target, flags, secondary effect, Z/Max variants, and effect text. Use `get_learnset` to check which Pokémon learn it and `calculate_damage` to apply it in a matchup, rather than reasoning about damage from these fields. Accepts Showdown move names case- and punctuation-insensitively ("make it rain"); unknown moves return an isError with near matches. Read-only and offline.',
|
|
344
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
345
|
+
inputSchema: {
|
|
346
|
+
move: z.string().describe('Move name, e.g. "Earthquake", "Make It Rain", "Dragon Claw".'),
|
|
347
|
+
generation: genSchema,
|
|
348
|
+
},
|
|
349
|
+
outputSchema: {
|
|
350
|
+
name: z.string().describe('Move name, e.g. "Earthquake".'),
|
|
351
|
+
num: z.number().describe('Move number in the dataset, which is also the sort key `search` returns moves by.'),
|
|
352
|
+
gen: z.number().describe('Generation the move was introduced in.'),
|
|
353
|
+
type: z.string().describe(`Move type. One of ${TYPE_NAMES}.`),
|
|
354
|
+
category: z.string().describe('Damage class: "Physical", "Special", or "Status".'),
|
|
355
|
+
basePower: z
|
|
356
|
+
.number()
|
|
357
|
+
.describe('Base power; 0 for status moves and for moves whose power is computed rather than fixed (e.g. Seismic Toss, Low Kick).'),
|
|
358
|
+
accuracy: z
|
|
359
|
+
.union([z.number(), z.boolean()])
|
|
360
|
+
.describe('Accuracy as a percentage, or true when the move cannot miss.'),
|
|
361
|
+
pp: z.number().describe('Base PP, before PP Ups.'),
|
|
362
|
+
priority: z.number().describe('Priority bracket: positive moves act first, negative moves last.'),
|
|
363
|
+
target: z
|
|
364
|
+
.string()
|
|
365
|
+
.describe('Targeting mode, e.g. "normal", "self", "allAdjacent", "allAdjacentFoes", "allySide".'),
|
|
366
|
+
flags: moveFlags,
|
|
367
|
+
shortDesc: z.string().describe('One-line effect summary.'),
|
|
368
|
+
desc: z.string().describe('Full effect description.'),
|
|
369
|
+
secondary: secondaryEffect
|
|
370
|
+
.optional()
|
|
371
|
+
.describe('Secondary effect attached to the move itself, when it has one (absent for moves with none).'),
|
|
372
|
+
secondaries: z
|
|
373
|
+
.array(secondaryEffect)
|
|
374
|
+
.optional()
|
|
375
|
+
.describe('List of secondary effects, present for moves that carry more than one (e.g. a different effect per hit); absent for moves with none.'),
|
|
376
|
+
isZ: z
|
|
377
|
+
.string()
|
|
378
|
+
.optional()
|
|
379
|
+
.describe('Z-Crystal id that turns this move into a Z-Move; absent when the move has no dedicated Z-Move.'),
|
|
380
|
+
zMove: z
|
|
381
|
+
.object({
|
|
382
|
+
basePower: z.number().optional().describe('Base power the Z-Move is fixed to.'),
|
|
383
|
+
effect: z.string().optional().describe('Effect id applied by the Z-Move, e.g. "clearnegativeboost".'),
|
|
384
|
+
boost: boostsTable.optional().describe('Stat boosts the Z-Move grants the user before attacking.'),
|
|
385
|
+
})
|
|
386
|
+
.optional()
|
|
387
|
+
.describe('The Z-Move this move becomes when a Z-Crystal is held; absent when it does not become one.'),
|
|
388
|
+
isMax: z
|
|
389
|
+
.union([z.string(), z.boolean()])
|
|
390
|
+
.optional()
|
|
391
|
+
.describe('Species name when this is a G-Max move, true for generic Max Moves; absent when the move is not a Max Move.'),
|
|
392
|
+
maxMove: z
|
|
393
|
+
.object({
|
|
394
|
+
basePower: z.number().describe('Base power of the Max Move under Dynamax.'),
|
|
395
|
+
})
|
|
396
|
+
.optional()
|
|
397
|
+
.describe('The Max Move this move becomes under Dynamax; absent when it does not become one.'),
|
|
398
|
+
breaksProtect: z
|
|
399
|
+
.boolean()
|
|
400
|
+
.optional()
|
|
401
|
+
.describe('true when the move hits through Protect and similar protection; absent when it does not.'),
|
|
402
|
+
drain: z
|
|
403
|
+
.array(z.number())
|
|
404
|
+
.optional()
|
|
405
|
+
.describe('HP the user recovers as a [numerator, denominator] fraction of damage dealt, e.g. [1, 2] for half; absent when the move does not drain.'),
|
|
406
|
+
recoil: z
|
|
407
|
+
.array(z.number())
|
|
408
|
+
.optional()
|
|
409
|
+
.describe('Recoil to the user as a [numerator, denominator] fraction of damage dealt, e.g. [33, 100]; absent when the move has no recoil.'),
|
|
410
|
+
multihit: z
|
|
411
|
+
.union([z.number(), z.array(z.number())])
|
|
412
|
+
.optional()
|
|
413
|
+
.describe('Hit count when the move hits multiple times: a fixed number, or a [min, max] range as [2, 5]; absent for single-hit moves.'),
|
|
414
|
+
alwaysHit: z
|
|
415
|
+
.boolean()
|
|
416
|
+
.optional()
|
|
417
|
+
.describe('true when the dataset marks the move as never missing; absent from every other move.'),
|
|
418
|
+
isNonstandard: z
|
|
419
|
+
.string()
|
|
420
|
+
.nullable()
|
|
421
|
+
.describe('"Past", "Future", "Unobtainable", or "CAP" when the move is not available in the current games; null when it is standard.'),
|
|
422
|
+
},
|
|
423
|
+
}, wrap(async (args) => {
|
|
424
|
+
const gen = normalizeGen(args.generation);
|
|
425
|
+
const dex = getDex(gen);
|
|
426
|
+
const m = dex.moves.get(args.move);
|
|
427
|
+
const suggestions = m.exists ? [] : fuzzyMatches(dex.moves.all().map((x) => x.id), dex.moves.all().map((x) => x.name), args.move);
|
|
428
|
+
return ok(moveToObj(requireExists(m, 'move', args.move, suggestions)));
|
|
429
|
+
}));
|
|
430
|
+
server.registerTool('get_item', {
|
|
431
|
+
title: 'Get item data',
|
|
432
|
+
description: 'Get one held item\'s data: effect text, category flags (Berry, Choice, Mega Stone, …), Z-move, Natural Gift, Fling, and flat stat boosts. Use it to confirm what an item actually does before recommending it; `get_set` returns the item a curated meta set runs. Accepts item names case-insensitively; unknown items return an isError with near matches. Read-only and offline.',
|
|
433
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
434
|
+
inputSchema: {
|
|
435
|
+
item: z.string().describe('Item name, e.g. "Choice Band", "Assault Vest", "Leftovers".'),
|
|
436
|
+
generation: genSchema,
|
|
437
|
+
},
|
|
438
|
+
outputSchema: {
|
|
439
|
+
name: z.string().describe('Item name, e.g. "Choice Band".'),
|
|
440
|
+
num: z.number().describe('Item number in the dataset; 0 for items that have none.'),
|
|
441
|
+
gen: z.number().describe('Generation the item was introduced in.'),
|
|
442
|
+
shortDesc: z.string().describe('One-line effect summary.'),
|
|
443
|
+
desc: z.string().describe('Full effect description.'),
|
|
444
|
+
isBerry: z.boolean().optional().describe('true when the item is a Berry (held and eaten on a trigger); absent otherwise.'),
|
|
445
|
+
isChoice: z.boolean().optional().describe('true when the item is a Choice item that locks the holder into one move; absent otherwise.'),
|
|
446
|
+
isGem: z.boolean().optional().describe('true when the item is a one-use type Gem that boosts a matching move; absent otherwise.'),
|
|
447
|
+
isPokeball: z.boolean().optional().describe('true when the item is a Poké Ball used for catching; absent otherwise.'),
|
|
448
|
+
megaStone: z
|
|
449
|
+
.record(z.string(), z.string())
|
|
450
|
+
.optional()
|
|
451
|
+
.describe('Mega Stone holders: species name to the Mega forme it unlocks, e.g. {"Garchomp": "Garchomp-Mega"}; absent when the item is not a Mega Stone.'),
|
|
452
|
+
zMove: z
|
|
453
|
+
.union([z.string(), z.boolean()])
|
|
454
|
+
.optional()
|
|
455
|
+
.describe('For Z-Crystals: the Z-Move it unlocks, or true for crystals whose move depends on the held move; absent when the item is not a Z-Crystal.'),
|
|
456
|
+
naturalGift: z
|
|
457
|
+
.object({
|
|
458
|
+
basePower: z.number().describe('Base power Natural Gift gains from this item.'),
|
|
459
|
+
type: z.string().describe('Type Natural Gift becomes with this item, e.g. "Fire".'),
|
|
460
|
+
})
|
|
461
|
+
.optional()
|
|
462
|
+
.describe('Natural Gift data, present only for Berries (the items Natural Gift can consume).'),
|
|
463
|
+
fling: z
|
|
464
|
+
.object({
|
|
465
|
+
basePower: z.number().describe('Base power Fling gains from this item.'),
|
|
466
|
+
status: z.string().optional().describe('Major status Fling inflicts on the target, when the item does (e.g. "par").'),
|
|
467
|
+
volatileStatus: z.string().optional().describe('Volatile status Fling inflicts on the target, when the item does (e.g. "flinch").'),
|
|
468
|
+
})
|
|
469
|
+
.optional()
|
|
470
|
+
.describe('Fling data, present only for items that can be flung.'),
|
|
471
|
+
boosts: boostsTable
|
|
472
|
+
.optional()
|
|
473
|
+
.describe('Flat stat stages the item grants while held, keyed by stat id, e.g. {"atk": 2} for Choice Band; absent for items that do not change stats directly.'),
|
|
474
|
+
forcedForme: z
|
|
475
|
+
.string()
|
|
476
|
+
.optional()
|
|
477
|
+
.describe('Forme this item forces on its holder, e.g. "Dialga-Origin" for Adamant Crystal; absent when the item changes no forme.'),
|
|
478
|
+
itemUser: z
|
|
479
|
+
.array(z.string())
|
|
480
|
+
.optional()
|
|
481
|
+
.describe('Species that can use the item where it is restricted to them; absent when any species can hold it.'),
|
|
482
|
+
isNonstandard: z
|
|
483
|
+
.string()
|
|
484
|
+
.nullable()
|
|
485
|
+
.describe('"Past", "Future", "Unobtainable", or "CAP" when the item is not available in the current games; null when it is standard.'),
|
|
486
|
+
},
|
|
487
|
+
}, wrap(async (args) => {
|
|
488
|
+
const gen = normalizeGen(args.generation);
|
|
489
|
+
const dex = getDex(gen);
|
|
490
|
+
const i = dex.items.get(args.item);
|
|
491
|
+
const suggestions = i.exists ? [] : fuzzyMatches(dex.items.all().map((x) => x.id), dex.items.all().map((x) => x.name), args.item);
|
|
492
|
+
return ok(itemToObj(requireExists(i, 'item', args.item, suggestions)));
|
|
493
|
+
}));
|
|
494
|
+
server.registerTool('get_ability', {
|
|
495
|
+
title: 'Get ability data',
|
|
496
|
+
description: 'Get one ability\'s effect text, flags, and the generations it exists in. Use it before relying on an ability in damage or speed reasoning; the set inputs of `calculate_damage` and `speed_check` take the ability or item name and apply it themselves. Accepts ability names case-insensitively; unknown abilities return an isError with near matches. Read-only and offline.',
|
|
497
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
498
|
+
inputSchema: {
|
|
499
|
+
ability: z.string().describe('Ability name, e.g. "Intimidate", "Protosynthesis".'),
|
|
500
|
+
generation: genSchema,
|
|
501
|
+
},
|
|
502
|
+
outputSchema: {
|
|
503
|
+
name: z.string().describe('Ability name, e.g. "Intimidate".'),
|
|
504
|
+
num: z.number().describe('Ability number in the dataset; 0 for abilities that have none.'),
|
|
505
|
+
gen: z.number().describe('Generation the ability was introduced in; 0 for the "No Ability" placeholder.'),
|
|
506
|
+
shortDesc: z.string().describe('One-line effect summary.'),
|
|
507
|
+
desc: z.string().describe('Full effect description.'),
|
|
508
|
+
flags: abilityFlags,
|
|
509
|
+
isNonstandard: z
|
|
510
|
+
.string()
|
|
511
|
+
.nullable()
|
|
512
|
+
.describe('"Past", "Future", "Unobtainable", or "CAP" when the ability is not available in the current games; null when it is standard.'),
|
|
513
|
+
},
|
|
514
|
+
}, wrap(async (args) => {
|
|
515
|
+
const gen = normalizeGen(args.generation);
|
|
516
|
+
const dex = getDex(gen);
|
|
517
|
+
const a = dex.abilities.get(args.ability);
|
|
518
|
+
const suggestions = a.exists ? [] : fuzzyMatches(dex.abilities.all().map((x) => x.id), dex.abilities.all().map((x) => x.name), args.ability);
|
|
519
|
+
return ok(abilityToObj(requireExists(a, 'ability', args.ability, suggestions)));
|
|
520
|
+
}));
|
|
521
|
+
server.registerTool('get_nature', {
|
|
522
|
+
title: 'Get nature effect',
|
|
523
|
+
description: 'Get one nature\'s stat effect: the stat it raises 10% and the stat it lowers 10%, or a neutral effect for the five natures that change nothing (Hardy, Docile, Serious, Bashful, Quirky). Use it when assembling a set, since `calculate_stats`, `calculate_damage`, `speed_check`, and `optimize_evs` all take a nature name rather than a numeric modifier. Accepts nature names case-insensitively; unknown natures return an isError. Read-only and offline.',
|
|
524
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
525
|
+
inputSchema: {
|
|
526
|
+
nature: z.string().describe('Nature name, e.g. "Jolly", "Timid", "Impish".'),
|
|
527
|
+
generation: genSchema,
|
|
528
|
+
},
|
|
529
|
+
outputSchema: {
|
|
530
|
+
name: z.string().describe('Nature name, e.g. "Jolly".'),
|
|
531
|
+
plus: z
|
|
532
|
+
.string()
|
|
533
|
+
.optional()
|
|
534
|
+
.describe('Stat raised 10%, as a stat id such as "spe"; absent for the five neutral natures that change nothing.'),
|
|
535
|
+
minus: z
|
|
536
|
+
.string()
|
|
537
|
+
.optional()
|
|
538
|
+
.describe('Stat lowered 10%, as a stat id such as "spa"; absent for the five neutral natures that change nothing.'),
|
|
539
|
+
gen: z.number().describe('Generation the nature system was introduced in.'),
|
|
540
|
+
},
|
|
541
|
+
}, wrap(async (args) => {
|
|
542
|
+
const gen = normalizeGen(args.generation);
|
|
543
|
+
const dex = getDex(gen);
|
|
544
|
+
const n = dex.natures.get(args.nature);
|
|
545
|
+
return ok(natureToObj(requireExists(n, 'nature', args.nature)));
|
|
546
|
+
}));
|
|
547
|
+
server.registerTool('get_learnset', {
|
|
548
|
+
title: 'Get a Pokémon learnset',
|
|
549
|
+
description: 'List every move a Pokémon can learn in a generation, grouped by acquisition method (level-up with the level, TM/TR, egg, tutor, event, and so on). Use it to validate moves before recommending a set; `check_legality` applies the same data when it flags moves a species cannot learn in a regulation. Accepts any species or form name; unknown species return an isError with near matches, and a species with no learnset data errors instead of returning an empty list. Read-only and offline.',
|
|
550
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
551
|
+
inputSchema: {
|
|
552
|
+
species: z.string().describe('Species or form name, e.g. "Garchomp", "Ogerpon-Wellspring".'),
|
|
553
|
+
generation: genSchema,
|
|
554
|
+
},
|
|
555
|
+
outputSchema: {
|
|
556
|
+
species: z.string().describe('Name of the species whose learnset this is, resolved from the argument, e.g. "Garchomp".'),
|
|
557
|
+
exists: z.boolean().describe('true when the dataset has learnset data for the species; a species without data errors instead of returning false.'),
|
|
558
|
+
eventOnly: z
|
|
559
|
+
.boolean()
|
|
560
|
+
.describe('true when the species is only obtainable through events, so most of its moves come from the event list.'),
|
|
561
|
+
eventData: z
|
|
562
|
+
.array(z.object({
|
|
563
|
+
generation: z.number().describe('Generation the event ran in.'),
|
|
564
|
+
level: z.number().describe('Level the event Pokémon is distributed at.'),
|
|
565
|
+
moves: z.array(z.string()).describe('Move ids the event Pokémon comes with.'),
|
|
566
|
+
pokeball: z.string().optional().describe('Ball the Pokémon is distributed in, as an id such as "cherishball".'),
|
|
567
|
+
shiny: z
|
|
568
|
+
.union([z.boolean(), z.number()])
|
|
569
|
+
.optional()
|
|
570
|
+
.describe('true (or 1) when the distributed Pokémon is shiny; absent when it is not.'),
|
|
571
|
+
gender: z.string().optional().describe('Gender the event forces, "M" or "F"; absent when the event does not fix it.'),
|
|
572
|
+
nature: z.string().optional().describe('Nature the event fixes; absent when the nature is not fixed.'),
|
|
573
|
+
isHidden: z.boolean().optional().describe('true when the event grants the hidden ability; absent otherwise.'),
|
|
574
|
+
abilities: z.array(z.string()).optional().describe('Ability ids the event Pokémon can come with.'),
|
|
575
|
+
ivs: z
|
|
576
|
+
.object({
|
|
577
|
+
hp: z.number().optional().describe('HP IV.'),
|
|
578
|
+
atk: z.number().optional().describe('Attack IV.'),
|
|
579
|
+
def: z.number().optional().describe('Defense IV.'),
|
|
580
|
+
spa: z.number().optional().describe('Special Attack IV.'),
|
|
581
|
+
spd: z.number().optional().describe('Special Defense IV.'),
|
|
582
|
+
spe: z.number().optional().describe('Speed IV.'),
|
|
583
|
+
})
|
|
584
|
+
.optional()
|
|
585
|
+
.describe('IVs the event fixes, keyed by stat id; only the stats the event pins down are listed.'),
|
|
586
|
+
perfectIVs: z.number().optional().describe('Number of stats guaranteed to be perfect (31 IVs); absent when the event guarantees none.'),
|
|
587
|
+
source: z.string().optional().describe('Source game the event belongs to, e.g. "gen8bdsp".'),
|
|
588
|
+
emeraldEventEgg: z.boolean().optional().describe('true when the event is the Emerald event egg; absent otherwise.'),
|
|
589
|
+
japan: z.boolean().optional().describe('true when the event was Japan-only; absent otherwise.'),
|
|
590
|
+
}))
|
|
591
|
+
.optional()
|
|
592
|
+
.describe('Event distributions that granted this species, when it has any; absent for species with no event history.'),
|
|
593
|
+
movesBySource: z
|
|
594
|
+
.record(z.string(), z.array(z.string()))
|
|
595
|
+
.describe('Learned moves grouped by how they are acquired, keyed by "Level-up", "TM", "Egg", "Tutor", "Event", "Raid/Event", "Virtual Console transfer", "Dream World", "Pre-evolution", or "Other"; a move learned more than one way appears under each. Each list is sorted by move name and holds display names, e.g. {"TM": ["Earthquake"], "Level-up": ["Dragon Claw"]}. Empty when the generation records no moves.'),
|
|
596
|
+
totalMoves: z.number().describe('Number of distinct moves the species can learn, counted before grouping by source.'),
|
|
597
|
+
},
|
|
598
|
+
}, wrap(async (args) => {
|
|
599
|
+
const gen = normalizeGen(args.generation);
|
|
600
|
+
const dex = getDex(gen);
|
|
601
|
+
const s = requireSpecies(dex, args.species);
|
|
602
|
+
const ls = await dex.learnsets.getByID(toID(s.name));
|
|
603
|
+
if (!ls.exists)
|
|
604
|
+
throw new Error(`No learnset data for "${args.species}".`);
|
|
605
|
+
return ok({ species: s.name, ...learnsetToObj(ls) });
|
|
606
|
+
}));
|
|
607
|
+
server.registerTool('get_type', {
|
|
608
|
+
title: 'Get type matchup chart entry',
|
|
609
|
+
description: 'Get the defensive profile of one type: what it is weak to, what it resists, what it is immune to, plus the Hidden Power IVs for that type. Use it for a type\'s own matchups; for one specific pairing pass attacker and defender to `type_chart`, and for a Pokémon\'s combined defensive chart give `type_chart` the species name instead. Accepts type names case-insensitively; unknown types return an isError. Read-only and offline.',
|
|
610
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
611
|
+
inputSchema: {
|
|
612
|
+
type: z.string().describe('Type name, e.g. "Steel", "Fairy", "Ground".'),
|
|
613
|
+
generation: genSchema,
|
|
614
|
+
},
|
|
615
|
+
outputSchema: {
|
|
616
|
+
name: z.string().describe('Type name, e.g. "Steel".'),
|
|
617
|
+
gen: z.number().describe('Always 0 in the bundled dataset — types are not stamped with the generation they were introduced in.'),
|
|
618
|
+
isNonstandard: z
|
|
619
|
+
.string()
|
|
620
|
+
.nullable()
|
|
621
|
+
.describe('"Future" when the type does not exist yet in the requested generation (e.g. Dark in generation 1) and "Past" when it no longer exists; null for types that are standard in that generation.'),
|
|
622
|
+
damageTaken: z
|
|
623
|
+
.record(z.string(), z.number())
|
|
624
|
+
.describe(`Multiplier this type takes from each attacking type, keyed by attacking type name. Values: 0 (immune), 0.5 (resisted), 1 (neutral), 2 (weak). Keys are ${TYPE_NAMES}; e.g. {"Fire": 2, "Ground": 2, "Water": 0.5} for Steel.`),
|
|
625
|
+
weaknesses: z
|
|
626
|
+
.array(z.string())
|
|
627
|
+
.describe(`Attacking types this type takes 2x from, e.g. ["Fire", "Ground"] for Steel; empty when it has none.`),
|
|
628
|
+
resistances: z
|
|
629
|
+
.array(z.string())
|
|
630
|
+
.describe('Attacking types this type takes 0.5x from; empty when it has none.'),
|
|
631
|
+
immunities: z
|
|
632
|
+
.array(z.string())
|
|
633
|
+
.describe('Attacking types this type takes 0x from, e.g. ["Poison"] for Steel; empty when it has none.'),
|
|
634
|
+
HPivs: z
|
|
635
|
+
.object({
|
|
636
|
+
hp: z.number().optional().describe('Required HP IV (30 or 31); absent when this Hidden Power type does not constrain it.'),
|
|
637
|
+
atk: z.number().optional().describe('Required Attack IV (30 or 31); absent when this Hidden Power type does not constrain it.'),
|
|
638
|
+
def: z.number().optional().describe('Required Defense IV (30 or 31); absent when this Hidden Power type does not constrain it.'),
|
|
639
|
+
spa: z.number().optional().describe('Required Special Attack IV (30 or 31); absent when this Hidden Power type does not constrain it.'),
|
|
640
|
+
spd: z.number().optional().describe('Required Special Defense IV (30 or 31); absent when this Hidden Power type does not constrain it.'),
|
|
641
|
+
spe: z.number().optional().describe('Required Speed IV (30 or 31); absent when this Hidden Power type does not constrain it.'),
|
|
642
|
+
})
|
|
643
|
+
.describe('IVs needed to make Hidden Power come out as this type, keyed by stat id; only the stats this type pins down are listed.'),
|
|
644
|
+
},
|
|
645
|
+
}, wrap(async (args) => {
|
|
646
|
+
const gen = normalizeGen(args.generation);
|
|
647
|
+
const dex = getDex(gen);
|
|
648
|
+
const t = dex.types.get(args.type);
|
|
649
|
+
return ok(typeToObj(requireExists(t, 'type', args.type)));
|
|
650
|
+
}));
|
|
651
|
+
server.registerTool('type_chart', {
|
|
652
|
+
title: 'Resolve type effectiveness',
|
|
653
|
+
description: 'Resolve type effectiveness in the mode the arguments imply: attacker + defender returns the single multiplier (the defender may be a type or a species, whose current-generation typing is used); attacker alone returns that type\'s offensive coverage against all 18 types; defender alone returns everything the defender takes, including 4x weaknesses and immunities; neither returns the complete 18x18 matrix. Use `get_type` for a type\'s own defensive entry, and `analyze_team` when the question spans a whole team. Names are case-insensitive; an unknown type or species returns an isError. Read-only and offline.',
|
|
654
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
655
|
+
inputSchema: {
|
|
656
|
+
attacker: z
|
|
657
|
+
.string()
|
|
658
|
+
.optional()
|
|
659
|
+
.describe('Attacking type name; omit to read the chart from the defender\'s side or to get the full matrix.'),
|
|
660
|
+
defender: z
|
|
661
|
+
.string()
|
|
662
|
+
.optional()
|
|
663
|
+
.describe('Defending type or species name; a species contributes its current-generation types.'),
|
|
664
|
+
generation: genSchema,
|
|
665
|
+
},
|
|
666
|
+
outputSchema: {
|
|
667
|
+
attacker: z
|
|
668
|
+
.string()
|
|
669
|
+
.optional()
|
|
670
|
+
.describe('The attacker as supplied, echoed back; absent when no attacker argument was given.'),
|
|
671
|
+
defender: z
|
|
672
|
+
.string()
|
|
673
|
+
.optional()
|
|
674
|
+
.describe('Resolved defender name — the type or species the multipliers were computed against; present in the matchup and defender-only replies.'),
|
|
675
|
+
defenderTypes: z
|
|
676
|
+
.array(z.string())
|
|
677
|
+
.optional()
|
|
678
|
+
.describe('The defending typing the multiplier was computed from, e.g. ["Dragon", "Ground"] for Garchomp or ["Steel"] for a type; present in the matchup and defender-only replies.'),
|
|
679
|
+
effectiveness: z
|
|
680
|
+
.number()
|
|
681
|
+
.optional()
|
|
682
|
+
.describe('Single multiplier for attacker against defender (0, 0.25, 0.5, 1, 2, or 4); present only when both attacker and defender were given.'),
|
|
683
|
+
label: z
|
|
684
|
+
.string()
|
|
685
|
+
.optional()
|
|
686
|
+
.describe('`effectiveness` in words, e.g. "2x super effective", "neutral", "immune"; present only when both attacker and defender were given.'),
|
|
687
|
+
coverage: z
|
|
688
|
+
.record(z.string(), effectivenessEntry)
|
|
689
|
+
.optional()
|
|
690
|
+
.describe(`Present only when an attacker was given without a defender: how that attacking type lands against each of the 18 defending types, keyed by defending type name (${TYPE_NAMES}).`),
|
|
691
|
+
damageTaken: z
|
|
692
|
+
.record(z.string(), effectivenessEntry)
|
|
693
|
+
.optional()
|
|
694
|
+
.describe('Present only when a defender was given without an attacker: everything the defender takes, keyed by attacking type name. Combined typing is folded in, so 4x and 0.25x entries appear for dual types and 0 marks immunities.'),
|
|
695
|
+
chart: z
|
|
696
|
+
.record(z.string(), z.record(z.string(), z.number()))
|
|
697
|
+
.optional()
|
|
698
|
+
.describe('Present only when neither argument was given: the full type chart as `chart[attackingType][defendingType] = multiplier`, each inner map keyed by the 18 defending type names with values 0, 0.25, 0.5, 1, 2, or 4.'),
|
|
699
|
+
note: z
|
|
700
|
+
.string()
|
|
701
|
+
.optional()
|
|
702
|
+
.describe('Present only alongside `chart`, spelling out how to read the full matrix (rows are attackers, columns are defenders) and the multiplier scale.'),
|
|
703
|
+
},
|
|
704
|
+
}, wrap(async (args) => {
|
|
705
|
+
const gen = normalizeGen(args.generation);
|
|
706
|
+
const dex = getDex(gen);
|
|
707
|
+
const resolveDefender = (name) => {
|
|
708
|
+
const t = dex.types.get(name);
|
|
709
|
+
if (t.exists)
|
|
710
|
+
return { label: t.name, types: [t.name] };
|
|
711
|
+
const s = dex.species.get(name);
|
|
712
|
+
if (s.exists)
|
|
713
|
+
return { label: s.name, types: [...s.types] };
|
|
714
|
+
throw new Error(`Unknown type or species "${name}".`);
|
|
715
|
+
};
|
|
716
|
+
if (args.attacker && args.defender) {
|
|
717
|
+
const def = resolveDefender(args.defender);
|
|
718
|
+
const mult = typeEffectiveness(args.attacker, def.types, gen);
|
|
719
|
+
return ok({
|
|
720
|
+
attacker: args.attacker,
|
|
721
|
+
defender: def.label,
|
|
722
|
+
defenderTypes: def.types,
|
|
723
|
+
effectiveness: mult,
|
|
724
|
+
label: effectivenessLabel(mult),
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
if (args.attacker) {
|
|
728
|
+
const coverage = {};
|
|
729
|
+
for (const t of TYPES18) {
|
|
730
|
+
const m = typeEffectiveness(args.attacker, [t], gen);
|
|
731
|
+
coverage[t] = { effectiveness: m, label: effectivenessLabel(m) };
|
|
732
|
+
}
|
|
733
|
+
return ok({ attacker: args.attacker, coverage });
|
|
734
|
+
}
|
|
735
|
+
if (args.defender) {
|
|
736
|
+
const def = resolveDefender(args.defender);
|
|
737
|
+
const taken = {};
|
|
738
|
+
for (const t of TYPES18) {
|
|
739
|
+
const m = typeEffectiveness(t, def.types, gen);
|
|
740
|
+
taken[t] = { effectiveness: m, label: effectivenessLabel(m) };
|
|
741
|
+
}
|
|
742
|
+
return ok({ defender: def.label, defenderTypes: def.types, damageTaken: taken });
|
|
743
|
+
}
|
|
744
|
+
const chart = {};
|
|
745
|
+
for (const atk of TYPES18) {
|
|
746
|
+
chart[atk] = {};
|
|
747
|
+
for (const def of TYPES18) {
|
|
748
|
+
chart[atk][def] = typeEffectiveness(atk, [def], gen);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return ok({ chart, note: 'Rows are attacking types, columns are defending types. Multipliers: 0 immune, 0.25, 0.5, 1, 2, 4.' });
|
|
752
|
+
}));
|
|
753
|
+
}
|
|
754
|
+
function effectivenessLabel(m) {
|
|
755
|
+
if (m === 0)
|
|
756
|
+
return 'immune';
|
|
757
|
+
if (m > 1)
|
|
758
|
+
return `${m}x super effective`;
|
|
759
|
+
if (m < 1)
|
|
760
|
+
return `${m}x not very effective`;
|
|
761
|
+
return 'neutral';
|
|
762
|
+
}
|
|
763
|
+
//# sourceMappingURL=data.js.map
|