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,718 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Battle mechanics tools: stat calculation, damage calculation, speed tiers.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { Move as CalcMove, calculate } from '@smogon/calc';
|
|
6
|
+
import { normalizeGen, getDex, statTable, damageResult, finalStat, buildPokemon, buildField, getCalcGen, STATS } from '../dex.js';
|
|
7
|
+
import { getRegulationSet } from '../regulations.js';
|
|
8
|
+
import { ok, wrap, requireExists, READ_ONLY_ANNOTATIONS } from '../result.js';
|
|
9
|
+
import { genSchema } from './schemas.js';
|
|
10
|
+
const evMap = z
|
|
11
|
+
.record(z.string(), z.number())
|
|
12
|
+
.optional()
|
|
13
|
+
.describe('EVs keyed by stat id (hp, atk, def, spa, spd, spe), each 0-252 in steps of 4; omitted stats are 0, and a total above 510 is rejected.');
|
|
14
|
+
const ivMap = z
|
|
15
|
+
.record(z.string(), z.number())
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('IVs keyed by stat id (hp, atk, def, spa, spd, spe), each 0-31; omitted stats default to 31.');
|
|
18
|
+
const boostMap = z
|
|
19
|
+
.record(z.string(), z.number())
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('Stat stages keyed by stat id (hp, atk, def, spa, spd, spe), each -6..+6; omitted stats are 0 (e.g. { atk: 2 } = +2 Attack).');
|
|
22
|
+
const setSchema = z.object({
|
|
23
|
+
species: z.string().describe('Species or form name, e.g. "Garchomp", "Ogerpon-Wellspring".'),
|
|
24
|
+
level: z
|
|
25
|
+
.number()
|
|
26
|
+
.int()
|
|
27
|
+
.min(1)
|
|
28
|
+
.max(100)
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('Level 1-100; defaults to 100 in the damage tools and 50 in the stat/speed tools.'),
|
|
31
|
+
nature: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Nature name, e.g. "Jolly", "Modest", "Adamant"; defaults to Serious (neutral) when omitted.'),
|
|
35
|
+
ivs: ivMap,
|
|
36
|
+
evs: evMap,
|
|
37
|
+
item: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('Held item name, e.g. "Choice Band", "Assault Vest", "Leftovers"; the calc applies its damage, Speed, or bulk effect.'),
|
|
41
|
+
ability: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('Ability name, e.g. "Intimidate", "Protosynthesis"; defaults to the species\u2019 default ability.'),
|
|
45
|
+
boosts: boostMap,
|
|
46
|
+
status: z
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Pre-existing status such as "brn", "par", or "tox"; burn halves physical damage, paralysis cuts Speed.'),
|
|
50
|
+
teraType: z.string().optional().describe('Tera type to use when the move is Terastallized, e.g. "Fairy".'),
|
|
51
|
+
abilityOn: z
|
|
52
|
+
.boolean()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe('Force the ability on (true) or off (false), e.g. to compare Protosynthesis active vs not; omitted leaves it to the calc.'),
|
|
55
|
+
isDynamaxed: z.boolean().optional().describe('Treat this Pokémon as Dynamaxed (doubles HP and alters several moves).'),
|
|
56
|
+
curHP: z.number().optional().describe('Current HP when entering damaged, e.g. 120; defaults to full HP.'),
|
|
57
|
+
moves: z
|
|
58
|
+
.array(z.string())
|
|
59
|
+
.optional()
|
|
60
|
+
.describe('Moveset names, e.g. ["Earthquake", "Dragon Claw"]; used by `calc_matchups` to pick the hardest-hitting move per defender.'),
|
|
61
|
+
});
|
|
62
|
+
/** Flatten a calc damage value (number | number[] | number[][]) into a flat roll list. */
|
|
63
|
+
function flatDamage(d) {
|
|
64
|
+
if (typeof d === 'number')
|
|
65
|
+
return [d];
|
|
66
|
+
if (Array.isArray(d))
|
|
67
|
+
return d.flat(Infinity).map(Number);
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
/** Stat-stage multiplier (e.g. +1 = 1.5x, +2 = 2x, -1 = 0.667x). */
|
|
71
|
+
function boostMult(stage) {
|
|
72
|
+
return stage >= 0 ? (2 + stage) / 2 : 2 / (2 - stage);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A six-stat block keyed by stat id (hp, atk, def, spa, spd, spe), always with
|
|
76
|
+
* all six keys: base stats, IVs, stat stages, and computed stats all arrive in
|
|
77
|
+
* this shape. `label` is prepended to each field description, so it must read
|
|
78
|
+
* as a noun phrase, e.g. "Base" -> "Base Attack: ...".
|
|
79
|
+
*/
|
|
80
|
+
function statBlock(label, description) {
|
|
81
|
+
return z
|
|
82
|
+
.object({
|
|
83
|
+
hp: z.number().describe(`${label} HP: hit points, which decide how much damage the set can take.`),
|
|
84
|
+
atk: z.number().describe(`${label} Attack: the stat behind physical damage dealt.`),
|
|
85
|
+
def: z.number().describe(`${label} Defense: the stat behind physical damage taken.`),
|
|
86
|
+
spa: z.number().describe(`${label} Special Attack: the stat behind special damage dealt.`),
|
|
87
|
+
spd: z.number().describe(`${label} Special Defense: the stat behind special damage taken.`),
|
|
88
|
+
spe: z.number().describe(`${label} Speed: turn order; the higher Speed moves first.`),
|
|
89
|
+
})
|
|
90
|
+
.describe(description);
|
|
91
|
+
}
|
|
92
|
+
/** EV map as `summarizeSet` builds it: unused stats are omitted, and generations 1-2 fix all six at 252. */
|
|
93
|
+
const reportedEvs = z
|
|
94
|
+
.record(z.string(), z.number())
|
|
95
|
+
.describe('EVs the set was calculated with, keyed by stat id (hp, atk, def, spa, spd, spe) with stats left at 0 omitted; generations 1-2 fix all six at 252 when the call supplies none.');
|
|
96
|
+
/** `[minimum, maximum]` damage of a resolved move, across all of its rolls. */
|
|
97
|
+
const damageRangeSchema = z
|
|
98
|
+
.tuple([z.number(), z.number()])
|
|
99
|
+
.describe('[minimum, maximum] damage: `calculate_damage` totals every roll (multi-hit moves are summed), while `calc_matchups` reports the flattened per-hit rolls, so its bounds stay single-hit values. [0, 0] means nothing could be calculated.');
|
|
100
|
+
/** One side of a damage calculation as the calc resolved it (see `summarizeSet` in dex.ts). */
|
|
101
|
+
const damageSetSchema = z.object({
|
|
102
|
+
species: z.string().describe('Canonical species name used in the calculation, e.g. "Garchomp".'),
|
|
103
|
+
level: z.number().int().describe('Level the set was calculated at; the damage tools default nested sets to level 100.'),
|
|
104
|
+
nature: z.string().describe('Nature the stats were computed with, e.g. "Jolly"; Serious when the call omitted one.'),
|
|
105
|
+
evs: reportedEvs,
|
|
106
|
+
ivs: statBlock('Individual value for', 'IVs the set was calculated with; all six keys are present, defaulting to 31.'),
|
|
107
|
+
item: z.string().optional().describe('Held item echoed back as supplied, e.g. "Choice Band", whose effect the calc applied; absent when the set carried none.'),
|
|
108
|
+
ability: z
|
|
109
|
+
.string()
|
|
110
|
+
.optional()
|
|
111
|
+
.describe('Ability used: the one supplied, else the species\u2019 first ability; absent for a species with no abilities.'),
|
|
112
|
+
teraType: z.string().optional().describe('Tera type echoed back when the call supplied one; absent otherwise.'),
|
|
113
|
+
status: z.string().optional().describe('Pre-existing status such as "brn", "par", or "tox"; absent when the set entered healthy.'),
|
|
114
|
+
boosts: statBlock('Stat stage for', 'Stat stages in effect for the calculation; all six keys are present with 0 for unboosted stats.'),
|
|
115
|
+
stats: statBlock('Final', 'The six stats of this set at its level, IVs, EVs, and nature; stat stages are applied inside the damage mechanics, so they are not folded in here.'),
|
|
116
|
+
});
|
|
117
|
+
export function registerCalcTools(server) {
|
|
118
|
+
server.registerTool('calculate_stats', {
|
|
119
|
+
title: 'Calculate final stats',
|
|
120
|
+
description: 'Compute one Pok\u00e9mon\u2019s final six stats at a level from its nature, IVs, and EVs, returning the stat table plus base stats and BST. Stats only, never a battle: use `calculate_damage` or `calc_matchups` for damage rolls, `speed_check` to place the Speed stat against a regulation roster, and `optimize_evs` when the spread must be derived from a goal. EVs are 0-252 per stat with a 510 total cap, IVs 0-31, level defaults to 50 and nature to Serious. Read-only and offline; unknown species or nature names return an isError.',
|
|
121
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
122
|
+
inputSchema: {
|
|
123
|
+
species: z.string().describe('Species or form name, e.g. "Garchomp", "Ogerpon-Wellspring".'),
|
|
124
|
+
level: z.number().int().min(1).max(100).default(50).describe('Level 1-100; default 50.'),
|
|
125
|
+
nature: z
|
|
126
|
+
.string()
|
|
127
|
+
.optional()
|
|
128
|
+
.describe('Nature name, e.g. "Jolly", "Modest"; default Serious (raises and lowers nothing).'),
|
|
129
|
+
evs: evMap,
|
|
130
|
+
ivs: ivMap,
|
|
131
|
+
generation: genSchema,
|
|
132
|
+
},
|
|
133
|
+
outputSchema: {
|
|
134
|
+
species: z.string().describe('Canonical species name the stats belong to, e.g. "Garchomp".'),
|
|
135
|
+
generation: z.number().int().describe('Generation whose base stats and mechanics were used, 1-9.'),
|
|
136
|
+
level: z.number().int().describe('Level the stats were computed at, 1-100.'),
|
|
137
|
+
nature: z.string().describe('Nature applied to the non-HP stats, e.g. "Jolly"; Serious when the call omitted one.'),
|
|
138
|
+
baseStats: statBlock('Base', 'The species\u2019 unmodified base stats, the same for every set of that species.'),
|
|
139
|
+
bst: z.number().describe('Base stat total: the sum of the six base stats, a rough measure of the species\u2019 overall power.'),
|
|
140
|
+
evs: statBlock('EV applied to', 'The EVs actually used per stat; all six keys are present, 0 for stats the call left uninvested.'),
|
|
141
|
+
ivs: statBlock('Individual value for', 'The IVs actually used per stat; all six keys are present, defaulting to 31.'),
|
|
142
|
+
stats: statBlock('Final', 'The six in-game stats this species reaches at this level, nature, IVs, and EVs; `hp` is the full HP stat, not a percentage.'),
|
|
143
|
+
},
|
|
144
|
+
}, wrap(async (args) => {
|
|
145
|
+
const gen = normalizeGen(args.generation);
|
|
146
|
+
const dex = getDex(gen);
|
|
147
|
+
const s = dex.species.get(args.species);
|
|
148
|
+
requireExists(s, 'Pokemon species', args.species);
|
|
149
|
+
const ivs = {};
|
|
150
|
+
const evs = {};
|
|
151
|
+
for (const st of STATS) {
|
|
152
|
+
ivs[st] = args.ivs?.[st] ?? 31;
|
|
153
|
+
evs[st] = args.evs?.[st] ?? 0;
|
|
154
|
+
if (ivs[st] < 0 || ivs[st] > 31)
|
|
155
|
+
throw new Error(`IV "${st}" must be 0-31.`);
|
|
156
|
+
if (evs[st] < 0 || evs[st] > 252)
|
|
157
|
+
throw new Error(`EV "${st}" must be 0-252.`);
|
|
158
|
+
}
|
|
159
|
+
const evTotal = STATS.reduce((sum, st) => sum + evs[st], 0);
|
|
160
|
+
if (evTotal > 510)
|
|
161
|
+
throw new Error(`EV total ${evTotal} exceeds 510.`);
|
|
162
|
+
const nature = args.nature ?? 'Serious';
|
|
163
|
+
const nat = dex.natures.get(nature);
|
|
164
|
+
requireExists(nat, 'nature', nature);
|
|
165
|
+
const stats = statTable(gen, s.baseStats, args.level, ivs, evs, nature);
|
|
166
|
+
return ok({
|
|
167
|
+
species: s.name,
|
|
168
|
+
generation: gen,
|
|
169
|
+
level: args.level,
|
|
170
|
+
nature: nature,
|
|
171
|
+
baseStats: s.baseStats,
|
|
172
|
+
bst: s.bst,
|
|
173
|
+
evs,
|
|
174
|
+
ivs,
|
|
175
|
+
stats,
|
|
176
|
+
});
|
|
177
|
+
}));
|
|
178
|
+
server.registerTool('calculate_damage', {
|
|
179
|
+
title: 'Calculate damage for one matchup',
|
|
180
|
+
description: 'Simulate one attack end to end: one attacker set, one defender set, one named move, optionally under weather, terrain, game type, or side conditions. Use `calc_matchups` when one attacker must be tested against several defenders, and `calculate_stats` for stat tables with no battle. `field.weather` takes Sand/Sun/Rain/Hail/Snow and `field.terrain` Electric/Grassy/Psychic/Misty; `attackerSide`/`defenderSide` take calc flags (isReflect, isLightScreen, isAuroraVeil, spikes 0-3, isSR), and set levels default to 100 here. Species and move names are validated first, so typos return an isError. Returns every damage roll, damageRange, koChance text, a description line, and both sets\u2019 computed stats. Read-only and offline.',
|
|
181
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
182
|
+
inputSchema: {
|
|
183
|
+
attacker: setSchema.describe('The attacking Pok\u00e9mon: species plus optional level, nature, IVs, EVs, item, ability, boosts, status, Tera type, and current HP.'),
|
|
184
|
+
defender: setSchema.describe('The defending Pok\u00e9mon, same fields as `attacker`; its Defense/SpD, HP, typing, and ability drive the result.'),
|
|
185
|
+
move: z.string().describe('Move used by the attacker, e.g. "Earthquake", "Make It Rain"; must be a real move name.'),
|
|
186
|
+
field: z
|
|
187
|
+
.object({
|
|
188
|
+
gameType: z.enum(['Singles', 'Doubles']).optional().describe('Doubles spreads damage across targets; default Singles.'),
|
|
189
|
+
weather: z.string().optional().describe('Weather: "Sand", "Sun", "Rain", "Hail", "Snow", "Harsh Sunshine", "Heavy Rain", or "Strong Winds"; default none.'),
|
|
190
|
+
terrain: z.string().optional().describe('Terrain: "Electric", "Grassy", "Psychic", or "Misty"; default none.'),
|
|
191
|
+
attackerSide: z
|
|
192
|
+
.record(z.string(), z.unknown())
|
|
193
|
+
.optional()
|
|
194
|
+
.describe('Attacker-side flags, e.g. { isHelpingHand: true, isTailwind: true, spikes: 2 }.'),
|
|
195
|
+
defenderSide: z
|
|
196
|
+
.record(z.string(), z.unknown())
|
|
197
|
+
.optional()
|
|
198
|
+
.describe('Defender-side flags, e.g. { isReflect: true, isLightScreen: true, isAuroraVeil: true, isSR: true }.'),
|
|
199
|
+
})
|
|
200
|
+
.optional()
|
|
201
|
+
.describe('Battlefield conditions applied to the calc; omit it for a neutral Singles field with no weather, terrain, or hazards.'),
|
|
202
|
+
generation: genSchema,
|
|
203
|
+
},
|
|
204
|
+
outputSchema: {
|
|
205
|
+
generation: z.number().int().describe('Generation whose data and mechanics were used, 1-9.'),
|
|
206
|
+
attacker: damageSetSchema.describe('The attacking set as the calc resolved it, including the six stats it swung with.'),
|
|
207
|
+
defender: damageSetSchema.describe('The defending set as the calc resolved it, including the six stats it was hit on.'),
|
|
208
|
+
move: z.string().describe('Canonical move name that was calculated, e.g. "Dragon Claw".'),
|
|
209
|
+
field: z
|
|
210
|
+
.object({
|
|
211
|
+
gameType: z.enum(['Singles', 'Doubles']).describe('How many targets the move hit; "Singles" unless the call asked for Doubles.'),
|
|
212
|
+
weather: z.string().optional().describe('Weather in effect, e.g. "Sun", "Rain", "Sand"; absent when the field had none.'),
|
|
213
|
+
terrain: z.string().optional().describe('Terrain in effect, e.g. "Electric", "Grassy"; absent when the field had none.'),
|
|
214
|
+
})
|
|
215
|
+
.describe('The battlefield the calc ran under, echoed back with its defaults filled in.'),
|
|
216
|
+
damage: z
|
|
217
|
+
.union([z.number(), z.array(z.number()), z.array(z.array(z.number()))])
|
|
218
|
+
.describe('Damage dealt by the attack: a single number for a straight-damage move (0 when the defender is immune), a flat list of rolls for a move that rolls its own damage (e.g. False Swipe), or one roll list per hit for a multi-hit move (e.g. Population Bomb, Dragon Darts).'),
|
|
219
|
+
damageRange: damageRangeSchema,
|
|
220
|
+
koChance: z
|
|
221
|
+
.string()
|
|
222
|
+
.optional()
|
|
223
|
+
.describe('Human-readable KO chance, e.g. "guaranteed OHKO" or "31.3% chance to 2HKO"; an empty string when no KO is possible (e.g. False Swipe), and absent when the calc could not describe the matchup at all, which is the immunity case.'),
|
|
224
|
+
description: z
|
|
225
|
+
.string()
|
|
226
|
+
.describe('One-line summary of the whole matchup, e.g. "252 Atk Choice Band Garchomp Dragon Claw vs. 252 HP / 252+ Def Corviknight: 64-76 (16.4 - 19.5%) -- possible 6HKO"; an explicit 0-damage note when the calc could not describe it.'),
|
|
227
|
+
},
|
|
228
|
+
}, wrap(async (args) => {
|
|
229
|
+
const gen = normalizeGen(args.generation);
|
|
230
|
+
// Validate species + move names for helpful errors before the calc throws.
|
|
231
|
+
const dex = getDex(gen);
|
|
232
|
+
requireExists(dex.species.get(args.attacker.species), 'Pokemon species', args.attacker.species);
|
|
233
|
+
requireExists(dex.species.get(args.defender.species), 'Pokemon species', args.defender.species);
|
|
234
|
+
requireExists(dex.moves.get(args.move), 'move', args.move);
|
|
235
|
+
const result = damageResult(gen, args.attacker, args.defender, args.move, args.field ?? {});
|
|
236
|
+
return ok(result);
|
|
237
|
+
}));
|
|
238
|
+
server.registerTool('calc_matchups', {
|
|
239
|
+
title: 'Batch damage matchups',
|
|
240
|
+
description: 'Run one attacker against 1-30 defenders in a single call, picking the hardest-hitting move per defender from `move` or `attacker.moves` and reporting each matchup\u2019s damage range, KO chance, immunity, and who moves first. Use `calculate_damage` for a single pinned matchup or when side screens and hazards matter (this tool\u2019s `field` has only gameType, weather, and terrain); use `analyze_team` for type-synergy, not damage. Supply `move` or a non-empty `attacker.moves`, else the call errors; defender levels default to 100. Read-only, offline, deterministic; unknown species or move names return an isError naming the offender.',
|
|
241
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
242
|
+
inputSchema: {
|
|
243
|
+
attacker: setSchema.describe('The single attacking Pok\u00e9mon; set `moves` to let the tool choose the best move against each defender.'),
|
|
244
|
+
move: z.string().optional().describe('Pin the matchup to this one move, e.g. "Close Combat"; when omitted, `attacker.moves` is searched instead.'),
|
|
245
|
+
defenders: z
|
|
246
|
+
.array(setSchema)
|
|
247
|
+
.min(1)
|
|
248
|
+
.max(30)
|
|
249
|
+
.describe('1-30 defender sets, each with the same fields as `attacker`; every entry is scored against the same attacker, move set, and field.'),
|
|
250
|
+
field: z
|
|
251
|
+
.object({
|
|
252
|
+
gameType: z.enum(['Singles', 'Doubles']).optional().describe('Doubles spreads damage across targets; default Singles.'),
|
|
253
|
+
weather: z.string().optional().describe('Weather: "Sand", "Sun", "Rain", "Hail", "Snow", "Harsh Sunshine", "Heavy Rain", or "Strong Winds"; default none.'),
|
|
254
|
+
terrain: z.string().optional().describe('Terrain: "Electric", "Grassy", "Psychic", or "Misty"; default none.'),
|
|
255
|
+
})
|
|
256
|
+
.optional()
|
|
257
|
+
.describe('Shared battlefield conditions for every matchup; omit for a neutral Singles field. Side hazards and screens are only available on `calculate_damage`.'),
|
|
258
|
+
generation: genSchema,
|
|
259
|
+
},
|
|
260
|
+
outputSchema: {
|
|
261
|
+
generation: z.number().int().describe('Generation whose data and mechanics were used, 1-9.'),
|
|
262
|
+
attacker: z.string().describe('Canonical species name of the single attacker every matchup was run with.'),
|
|
263
|
+
move: z
|
|
264
|
+
.string()
|
|
265
|
+
.describe('The fixed move both sides were scored with, or the literal "best of moveset" when the tool picked the hardest-hitting move per defender.'),
|
|
266
|
+
matchups: z
|
|
267
|
+
.array(z.object({
|
|
268
|
+
defender: z.string().describe('Canonical species name of the defending set.'),
|
|
269
|
+
bestMove: z
|
|
270
|
+
.string()
|
|
271
|
+
.nullable()
|
|
272
|
+
.describe('The hardest-hitting move of the supplied move set against this defender, or null when none of them could be calculated (e.g. every move is unsupported in this generation).'),
|
|
273
|
+
damageRange: damageRangeSchema,
|
|
274
|
+
koChance: z
|
|
275
|
+
.string()
|
|
276
|
+
.optional()
|
|
277
|
+
.describe('KO chance of `bestMove` against this defender, e.g. "guaranteed OHKO", or an empty string when the calc reports no KO; absent when there is no usable move.'),
|
|
278
|
+
description: z
|
|
279
|
+
.string()
|
|
280
|
+
.describe('One-line summary of `bestMove` against this defender, e.g. "252 Atk Garchomp Earthquake vs. 252 HP / 252+ Def Corviknight: 108-128 (27.9 - 33.1%)"; when no move resolved it explains that instead.'),
|
|
281
|
+
immune: z
|
|
282
|
+
.boolean()
|
|
283
|
+
.describe('True when the best move\u2019s maximum roll is 0 — the defender takes nothing from every move tried, so the matchup is unwinnable with this move set.'),
|
|
284
|
+
speed: z
|
|
285
|
+
.object({
|
|
286
|
+
attacker: z.number().describe('The attacker\u2019s final Speed stat, the same number in every matchup.'),
|
|
287
|
+
defender: z.number().describe('This defender\u2019s final Speed stat.'),
|
|
288
|
+
attackerMovesFirst: z
|
|
289
|
+
.boolean()
|
|
290
|
+
.describe('True when the attacker\u2019s Speed is greater than or equal to the defender\u2019s, so the attacker moves first; from raw Speed stats only, so boosts, items, and paralysis are ignored.'),
|
|
291
|
+
})
|
|
292
|
+
.describe('Who moves first in this matchup, from the two Speed stats alone.'),
|
|
293
|
+
}))
|
|
294
|
+
.describe('One entry per defender, in the order the defenders were supplied.'),
|
|
295
|
+
},
|
|
296
|
+
}, wrap(async (args) => {
|
|
297
|
+
const gen = normalizeGen(args.generation);
|
|
298
|
+
const dex = getDex(gen);
|
|
299
|
+
requireExists(dex.species.get(args.attacker.species), 'Pokemon species', args.attacker.species);
|
|
300
|
+
const moveNames = args.move ? [args.move] : (args.attacker.moves ?? []);
|
|
301
|
+
if (moveNames.length === 0) {
|
|
302
|
+
throw new Error('Provide `move`, or `attacker.moves` to pick the best move per defender.');
|
|
303
|
+
}
|
|
304
|
+
for (const mv of moveNames)
|
|
305
|
+
requireExists(dex.moves.get(mv), 'move', mv);
|
|
306
|
+
const attacker = buildPokemon(gen, args.attacker);
|
|
307
|
+
const field = buildField(args.field ?? {});
|
|
308
|
+
const genCalc = getCalcGen(gen);
|
|
309
|
+
const atkSpe = attacker.stats.spe;
|
|
310
|
+
const matchups = [];
|
|
311
|
+
for (const defSpec of args.defenders) {
|
|
312
|
+
requireExists(dex.species.get(defSpec.species), 'Pokemon species', defSpec.species);
|
|
313
|
+
const defender = buildPokemon(gen, defSpec);
|
|
314
|
+
let best = null;
|
|
315
|
+
for (const mvName of moveNames) {
|
|
316
|
+
const mv = new CalcMove(genCalc, mvName);
|
|
317
|
+
let result;
|
|
318
|
+
try {
|
|
319
|
+
result = calculate(genCalc, attacker, defender, mv, field);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const flat = flatDamage(result.damage);
|
|
325
|
+
const maxDmg = flat.length ? Math.max(...flat) : 0;
|
|
326
|
+
if (best === null || maxDmg > best.maxDmg) {
|
|
327
|
+
let desc = '';
|
|
328
|
+
let ko;
|
|
329
|
+
try {
|
|
330
|
+
desc = result.desc();
|
|
331
|
+
ko = result.kochance().text;
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
desc = `${attacker.name} ${mv.name} vs. ${defender.name}: 0 damage (immune).`;
|
|
335
|
+
}
|
|
336
|
+
best = {
|
|
337
|
+
move: mv.name,
|
|
338
|
+
maxDmg,
|
|
339
|
+
range: flat.length ? [Math.min(...flat), Math.max(...flat)] : [0, 0],
|
|
340
|
+
desc,
|
|
341
|
+
ko,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const defSpe = defender.stats.spe;
|
|
346
|
+
matchups.push({
|
|
347
|
+
defender: defender.name,
|
|
348
|
+
bestMove: best ? best.move : null,
|
|
349
|
+
damageRange: best ? best.range : [0, 0],
|
|
350
|
+
koChance: best?.ko,
|
|
351
|
+
description: best ? best.desc : 'No damage-dealing move resolved.',
|
|
352
|
+
immune: best ? best.maxDmg === 0 : true,
|
|
353
|
+
speed: { attacker: atkSpe, defender: defSpe, attackerMovesFirst: atkSpe >= defSpe },
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return ok({
|
|
357
|
+
generation: gen,
|
|
358
|
+
attacker: attacker.name,
|
|
359
|
+
move: args.move ?? 'best of moveset',
|
|
360
|
+
matchups,
|
|
361
|
+
});
|
|
362
|
+
}));
|
|
363
|
+
server.registerTool('speed_check', {
|
|
364
|
+
title: 'Check Speed against a regulation',
|
|
365
|
+
description: 'Compute one Pok\u00e9mon\u2019s final Speed and, given a Regulation Set, rank it against that roster at its fastest (252 EV, +Spe nature) and uninvested reference speeds. Speed only: for damage use `calculate_damage` or `calc_matchups`, for a tier-wide ranking use `speed_tiers`, and to find the Speed EVs that beat a target use `optimize_evs`. Applies `boosts.spe` (-6..+6) and Choice Scarf \u00d71.5; other items are reported as Speed-neutral, and `regulation` is optional. Returns finalSpeed, modifiers, and outspeeds/conditional/losesTo counts with up to 15 threats each. Read-only and offline; unknown names return an isError.',
|
|
366
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
367
|
+
inputSchema: {
|
|
368
|
+
species: z.string().describe('Species or form name, e.g. "Dragapult", "Ogerpon-Wellspring".'),
|
|
369
|
+
level: z.number().int().min(1).max(100).default(50).describe('Level 1-100; default 50, matching VGC play.'),
|
|
370
|
+
nature: z.string().optional().describe('Nature name, e.g. "Jolly", "Timid"; default Serious (neutral Speed).'),
|
|
371
|
+
evs: evMap.describe('EVs keyed by stat id; only `spe` (0-252) changes the result, e.g. { spe: 252 }.'),
|
|
372
|
+
ivs: ivMap.describe('IVs keyed by stat id; only `spe` (0-31) changes the result, and it defaults to 31.'),
|
|
373
|
+
boosts: boostMap.describe('Stat stages; only `spe` (-6..+6) is applied, e.g. { spe: 1 } for a +1 Speed stage.'),
|
|
374
|
+
item: z.string().optional().describe('Held item, e.g. "Choice Scarf"; only Choice Scarf multiplies Speed (\u00d71.5), other items are listed as speed-neutral.'),
|
|
375
|
+
regulation: z
|
|
376
|
+
.string()
|
|
377
|
+
.optional()
|
|
378
|
+
.describe('Regulation Set name or id from `list_regulations` to compare against, e.g. "Regulation Set G"; omit to get the raw Speed only.'),
|
|
379
|
+
generation: genSchema,
|
|
380
|
+
},
|
|
381
|
+
outputSchema: {
|
|
382
|
+
species: z.string().describe('Canonical species name the Speed belongs to, e.g. "Dragapult".'),
|
|
383
|
+
generation: z.number().int().describe('Generation whose base stats and mechanics were used, 1-9.'),
|
|
384
|
+
level: z.number().int().describe('Level the Speed was computed at, 1-100.'),
|
|
385
|
+
nature: z.string().describe('Nature applied Speed, e.g. "Jolly" for +Spe; Serious when the call omitted one.'),
|
|
386
|
+
baseSpe: z.number().describe('The species\u2019 base Speed stat, before level, IVs, EVs, nature, item, and boosts.'),
|
|
387
|
+
evSpe: z.number().int().describe('Speed EVs invested, 0-252.'),
|
|
388
|
+
ivSpe: z.number().int().describe('Speed IV used, 0-31.'),
|
|
389
|
+
finalSpeed: z
|
|
390
|
+
.number()
|
|
391
|
+
.int()
|
|
392
|
+
.describe('The final Speed stat after level, IVs, EVs, nature, Speed stage, and item; this is the number turn order compares.'),
|
|
393
|
+
modifiers: z
|
|
394
|
+
.array(z.string())
|
|
395
|
+
.describe('Human-readable list of everything that changed the Speed stat, e.g. ["Speed stage +1", "Choice Scarf x1.5"]; a speed-neutral item is noted here too, and the list is empty when nothing applied.'),
|
|
396
|
+
comparison: z
|
|
397
|
+
.object({
|
|
398
|
+
regulation: z.string().describe('Canonical name of the Regulation Set this Speed was ranked against.'),
|
|
399
|
+
yourSpeed: z.number().describe('Your final Speed, repeated so the comparison reads on its own.'),
|
|
400
|
+
outspeeds: z
|
|
401
|
+
.object({
|
|
402
|
+
count: z.number().int().describe('How many eligible species you outspeed even at their fastest.'),
|
|
403
|
+
threats: z
|
|
404
|
+
.array(z.object({
|
|
405
|
+
species: z.string().describe('A species you outspeed.'),
|
|
406
|
+
baseSpe: z.number().describe('Its base Speed stat, for a quick sense of the gap.'),
|
|
407
|
+
maxSpe: z.number().describe('Its fastest possible Speed at this level (252 Speed EVs, +Spe nature), which your Speed still beats.'),
|
|
408
|
+
}))
|
|
409
|
+
.describe('Up to 15 of the species you always outspeed, fastest first; check `count` for the full total.'),
|
|
410
|
+
})
|
|
411
|
+
.describe('Roster species you move before no matter how they invest.'),
|
|
412
|
+
conditional: z
|
|
413
|
+
.object({
|
|
414
|
+
count: z.number().int().describe('How many eligible species sit in the overlap band between your Speed and their range.'),
|
|
415
|
+
threats: z
|
|
416
|
+
.array(z.object({
|
|
417
|
+
species: z.string().describe('A species whose Speed can be either side of yours.'),
|
|
418
|
+
baseSpe: z.number().describe('Its base Speed stat.'),
|
|
419
|
+
maxSpe: z.number().describe('Its fastest possible Speed at this level (252 Speed EVs, +Spe nature), above your Speed.'),
|
|
420
|
+
minSpe: z.number().describe('Its Speed with no investment (0 EVs, neutral nature), below your Speed.'),
|
|
421
|
+
}))
|
|
422
|
+
.describe('Up to 15 of those species, fastest first; you beat an uninvested one but lose to a fully invested one.'),
|
|
423
|
+
})
|
|
424
|
+
.describe('Species whose Speed straddles yours, so the order depends on their spread.'),
|
|
425
|
+
losesTo: z
|
|
426
|
+
.object({
|
|
427
|
+
count: z.number().int().describe('How many eligible species are still faster than you even when they invest nothing.'),
|
|
428
|
+
threats: z
|
|
429
|
+
.array(z.object({
|
|
430
|
+
species: z.string().describe('A species that outspeeds you.'),
|
|
431
|
+
baseSpe: z.number().describe('Its base Speed stat.'),
|
|
432
|
+
minSpe: z.number().describe('Its Speed with no investment (0 EVs, neutral nature), still above your Speed.'),
|
|
433
|
+
}))
|
|
434
|
+
.describe('Up to 15 of those species, highest Speed first; `count` gives the full total.'),
|
|
435
|
+
})
|
|
436
|
+
.describe('Roster species you cannot outrun even when they are uninvested.'),
|
|
437
|
+
})
|
|
438
|
+
.optional()
|
|
439
|
+
.describe('Present only when `regulation` was supplied: how this Speed places against that roster, whose threat lists are each capped at 15 entries.'),
|
|
440
|
+
},
|
|
441
|
+
}, wrap(async (args) => {
|
|
442
|
+
const gen = normalizeGen(args.generation);
|
|
443
|
+
const dex = getDex(gen);
|
|
444
|
+
const sp = dex.species.get(args.species);
|
|
445
|
+
requireExists(sp, 'Pokemon species', args.species);
|
|
446
|
+
const nature = args.nature ?? 'Serious';
|
|
447
|
+
requireExists(dex.natures.get(nature), 'nature', nature);
|
|
448
|
+
const speEV = args.evs?.spe ?? 0;
|
|
449
|
+
const speIV = args.ivs?.spe ?? 31;
|
|
450
|
+
const boost = args.boosts?.spe ?? 0;
|
|
451
|
+
if (speEV < 0 || speEV > 252)
|
|
452
|
+
throw new Error('EV "spe" must be 0-252.');
|
|
453
|
+
if (speIV < 0 || speIV > 31)
|
|
454
|
+
throw new Error('IV "spe" must be 0-31.');
|
|
455
|
+
if (boost < -6 || boost > 6)
|
|
456
|
+
throw new Error('Boost "spe" must be -6..6.');
|
|
457
|
+
let speed = finalStat(gen, 'spe', sp.baseStats.spe, speIV, speEV, args.level, nature);
|
|
458
|
+
const modifiers = [];
|
|
459
|
+
if (boost !== 0) {
|
|
460
|
+
speed = Math.floor(speed * boostMult(boost));
|
|
461
|
+
modifiers.push(`Speed stage ${boost > 0 ? '+' : ''}${boost}`);
|
|
462
|
+
}
|
|
463
|
+
if (args.item) {
|
|
464
|
+
const it = dex.items.get(args.item);
|
|
465
|
+
requireExists(it, 'item', args.item);
|
|
466
|
+
if (it.id === 'choicescarf') {
|
|
467
|
+
speed = Math.floor(speed * 1.5);
|
|
468
|
+
modifiers.push('Choice Scarf x1.5');
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
modifiers.push(`item "${it.name}" (no speed effect)`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
let comparison;
|
|
475
|
+
if (args.regulation) {
|
|
476
|
+
const set = getRegulationSet(args.regulation);
|
|
477
|
+
if (!set)
|
|
478
|
+
throw new Error(`Unknown regulation set "${args.regulation}".`);
|
|
479
|
+
const outspeeds = [];
|
|
480
|
+
const conditional = [];
|
|
481
|
+
const losesTo = [];
|
|
482
|
+
for (const name of set.eligibleSpecies) {
|
|
483
|
+
const s = dex.species.get(name);
|
|
484
|
+
if (!s.exists)
|
|
485
|
+
continue;
|
|
486
|
+
const base = s.baseStats.spe;
|
|
487
|
+
const maxSpe = finalStat(gen, 'spe', base, 31, 252, args.level, 'Jolly');
|
|
488
|
+
const minSpe = finalStat(gen, 'spe', base, 31, 0, args.level, 'Serious');
|
|
489
|
+
if (speed > maxSpe)
|
|
490
|
+
outspeeds.push({ species: s.name, baseSpe: base, maxSpe });
|
|
491
|
+
else if (speed < minSpe)
|
|
492
|
+
losesTo.push({ species: s.name, baseSpe: base, minSpe });
|
|
493
|
+
else
|
|
494
|
+
conditional.push({ species: s.name, baseSpe: base, maxSpe, minSpe });
|
|
495
|
+
}
|
|
496
|
+
const byMax = (a, b) => b.maxSpe - a.maxSpe;
|
|
497
|
+
outspeeds.sort(byMax);
|
|
498
|
+
conditional.sort(byMax);
|
|
499
|
+
losesTo.sort((a, b) => b.minSpe - a.minSpe);
|
|
500
|
+
comparison = {
|
|
501
|
+
regulation: set.name,
|
|
502
|
+
yourSpeed: speed,
|
|
503
|
+
outspeeds: { count: outspeeds.length, threats: outspeeds.slice(0, 15) },
|
|
504
|
+
conditional: { count: conditional.length, threats: conditional.slice(0, 15) },
|
|
505
|
+
losesTo: { count: losesTo.length, threats: losesTo.slice(0, 15) },
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
return ok({
|
|
509
|
+
species: sp.name,
|
|
510
|
+
generation: gen,
|
|
511
|
+
level: args.level,
|
|
512
|
+
nature,
|
|
513
|
+
baseSpe: sp.baseStats.spe,
|
|
514
|
+
evSpe: speEV,
|
|
515
|
+
ivSpe: speIV,
|
|
516
|
+
finalSpeed: speed,
|
|
517
|
+
modifiers,
|
|
518
|
+
comparison,
|
|
519
|
+
});
|
|
520
|
+
}));
|
|
521
|
+
server.registerTool('optimize_evs', {
|
|
522
|
+
title: 'Optimize EVs for a goal',
|
|
523
|
+
description: 'Derive a minimal EV spread for one Pok\u00e9mon satisfying up to three goals: survive a named attack, outspeed a target Speed, and guarantee a KO in 1-4 hits. Use it when EVs must come from a goal \u2014 `calculate_stats` evaluates a spread you already have, `speed_check` ranks Speed without deriving EVs, and `get_set` returns a curated spread. Supplying none of survive/outspeed/kill errors; `outspeed` takes a set `target` or a raw `speed`, and leftover EVs fill `maximize` (default spe). Returns the spread, resulting stats, totalEVs/unusedEVs of the 508 usable, and a verification line per goal. Read-only and offline; an unreachable goal returns an isError.',
|
|
524
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
525
|
+
inputSchema: {
|
|
526
|
+
species: z.string().describe('Species or form name to optimize, e.g. "Garchomp", "Incineroar".'),
|
|
527
|
+
level: z.number().int().min(1).max(100).default(50).describe('Level 1-100; default 50 (VGC), where bulk and Speed benchmarks are tightest.'),
|
|
528
|
+
nature: z
|
|
529
|
+
.string()
|
|
530
|
+
.optional()
|
|
531
|
+
.describe('Nature used for every stat calculation, e.g. "Adamant", "Calm"; default Serious. Change it to trade one stat for another.'),
|
|
532
|
+
ivs: ivMap.describe('IVs to hold fixed while searching, keyed by stat id; omitted stats default to 31 (use 0 for a Trick Room Speed IV).'),
|
|
533
|
+
item: z.string().optional().describe('Item held while solving, e.g. "Assault Vest"; it changes the bulk or Speed the goals are tested against.'),
|
|
534
|
+
ability: z.string().optional().describe('Ability assumed active while solving, e.g. "Intimidate", "Protosynthesis".'),
|
|
535
|
+
survive: z
|
|
536
|
+
.object({
|
|
537
|
+
attacker: setSchema.describe('The attacker whose move must be survived; the calc picks Defense or SpD from the move\u2019s category. Set its `level` to match the optimized Pok\u00e9mon\u2019s, since a nested set defaults to level 100.'),
|
|
538
|
+
move: z.string().describe('Move being survived, e.g. "Close Combat"; a valid move name is required.'),
|
|
539
|
+
})
|
|
540
|
+
.optional()
|
|
541
|
+
.describe('Add a "always live this hit" goal: the search minimizes HP plus the relevant Defense EVs that keep the worst roll below max HP.'),
|
|
542
|
+
outspeed: z
|
|
543
|
+
.object({
|
|
544
|
+
target: setSchema
|
|
545
|
+
.optional()
|
|
546
|
+
.describe('Set to outspeed, e.g. {"species": "Dragapult", "nature": "Jolly", "evs": {"spe": 252}}; its computed Speed becomes the benchmark. Give it the same `level` as the optimized Pok\u00e9mon, since a nested set defaults to level 100.'),
|
|
547
|
+
speed: z.number().int().min(1).optional().describe('Raw Speed number to beat when there is no full target set, e.g. 189.'),
|
|
548
|
+
})
|
|
549
|
+
.optional()
|
|
550
|
+
.describe('Add an outspeed goal: supply `target` or `speed` (one is required here, otherwise that goal errors).'),
|
|
551
|
+
kill: z
|
|
552
|
+
.object({
|
|
553
|
+
target: setSchema.describe('The defender that must be KOed, including its bulk EVs, item, and ability; give it the same `level` as the optimized Pok\u00e9mon, since a nested set defaults to level 100.'),
|
|
554
|
+
move: z.string().describe('Move used for the KO, e.g. "Knock Off"; its category decides whether Atk or SpA EVs are minimized.'),
|
|
555
|
+
hits: z.number().int().min(1).max(4).default(1).describe('Number of hits the move must KO in, 1-4 (default 1); each hit must reach target max HP / hits.'),
|
|
556
|
+
})
|
|
557
|
+
.optional()
|
|
558
|
+
.describe('Add a KO goal: minimizes Atk or SpA so that even the lowest damage roll reaches the per-hit HP threshold.'),
|
|
559
|
+
maximize: z
|
|
560
|
+
.enum(['atk', 'spa', 'spe', 'hp', 'def', 'spd'])
|
|
561
|
+
.default('spe')
|
|
562
|
+
.describe('Stat that receives leftover EVs after the goals are met, capped at 252 (default spe); EVs are added in steps of 4.'),
|
|
563
|
+
field: z
|
|
564
|
+
.object({
|
|
565
|
+
gameType: z.enum(['Singles', 'Doubles']).optional().describe('Doubles spreads damage across targets; default Singles.'),
|
|
566
|
+
weather: z.string().optional().describe('Weather for the survive/kill calcs, e.g. "Sun", "Rain", "Sand"; default none.'),
|
|
567
|
+
terrain: z.string().optional().describe('Terrain for the survive/kill calcs: "Electric", "Grassy", "Psychic", or "Misty"; default none.'),
|
|
568
|
+
})
|
|
569
|
+
.optional()
|
|
570
|
+
.describe('Battlefield conditions applied while testing the survive and kill goals; omit for a neutral Singles field.'),
|
|
571
|
+
generation: genSchema,
|
|
572
|
+
},
|
|
573
|
+
outputSchema: {
|
|
574
|
+
species: z.string().describe('Canonical species name the spread was solved for.'),
|
|
575
|
+
generation: z.number().int().describe('Generation whose data and mechanics were used, 1-9.'),
|
|
576
|
+
level: z.number().int().describe('Level every stat was computed at, 1-100.'),
|
|
577
|
+
nature: z.string().describe('Nature the spread was solved with, e.g. "Adamant"; Serious when the call omitted one.'),
|
|
578
|
+
item: z.string().optional().describe('Held item assumed while solving, as supplied; absent when the call gave none.'),
|
|
579
|
+
evs: statBlock('EV assigned to', 'The solved spread: all six keys, each a multiple of 4 from 0 to 252, with the `maximize` stat holding the leftover EVs.'),
|
|
580
|
+
stats: statBlock('Final', 'The six stats this exact spread reaches at this level and nature; recompute with `calculate_stats` to check a different spread.'),
|
|
581
|
+
totalEVs: z.number().int().describe('Sum of the six solved EVs, spent in multiples of 4 so 508 is the practical maximum.'),
|
|
582
|
+
unusedEVs: z.number().int().describe('EVs left over after the goals and the maximize step: 508 minus `totalEVs`, never negative.'),
|
|
583
|
+
verification: z
|
|
584
|
+
.array(z.string())
|
|
585
|
+
.describe('One line per goal that was solved, e.g. "survive: Dragapult Dragon Darts -> 96-114 vs 175 HP (max 65%)" or "kill: 132 ATK EVs -> 187-221 vs 175 HP (min 100%)"; the evidence that the spread meets each goal.'),
|
|
586
|
+
note: z
|
|
587
|
+
.string()
|
|
588
|
+
.describe('Caveat about how the spread was built: EVs come in steps of 4 (508 usable of 510), the maximize stat is capped at 252, and unused EVs can be reallocated by hand.'),
|
|
589
|
+
},
|
|
590
|
+
}, wrap(async (args) => {
|
|
591
|
+
const gen = normalizeGen(args.generation);
|
|
592
|
+
const dex = getDex(gen);
|
|
593
|
+
const sp = dex.species.get(args.species);
|
|
594
|
+
requireExists(sp, 'Pokemon species', args.species);
|
|
595
|
+
const nature = args.nature ?? 'Serious';
|
|
596
|
+
requireExists(dex.natures.get(nature), 'nature', nature);
|
|
597
|
+
if (!args.survive && !args.outspeed && !args.kill) {
|
|
598
|
+
throw new Error('Provide at least one goal: survive, outspeed, or kill.');
|
|
599
|
+
}
|
|
600
|
+
const ivs = {};
|
|
601
|
+
for (const st of STATS)
|
|
602
|
+
ivs[st] = args.ivs?.[st] ?? 31;
|
|
603
|
+
const genCalc = getCalcGen(gen);
|
|
604
|
+
const field = buildField(args.field ?? {});
|
|
605
|
+
const base = {
|
|
606
|
+
species: args.species,
|
|
607
|
+
level: args.level,
|
|
608
|
+
nature,
|
|
609
|
+
ivs,
|
|
610
|
+
item: args.item,
|
|
611
|
+
ability: args.ability,
|
|
612
|
+
};
|
|
613
|
+
const build = (evs) => buildPokemon(gen, { ...base, evs });
|
|
614
|
+
const required = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
|
|
615
|
+
const verification = [];
|
|
616
|
+
if (args.survive) {
|
|
617
|
+
const mv = dex.moves.get(args.survive.move);
|
|
618
|
+
requireExists(mv, 'move', args.survive.move);
|
|
619
|
+
const atk = buildPokemon(gen, args.survive.attacker);
|
|
620
|
+
const calcMv = new CalcMove(genCalc, args.survive.move);
|
|
621
|
+
const defStat = mv.category === 'Special' ? 'spd' : 'def';
|
|
622
|
+
let best = null;
|
|
623
|
+
let range = [0, 0];
|
|
624
|
+
let hp0 = 0;
|
|
625
|
+
for (let hp = 0; hp <= 252; hp += 4) {
|
|
626
|
+
for (let d = 0; d <= 252; d += 4) {
|
|
627
|
+
const defender = build({ hp, [defStat]: d });
|
|
628
|
+
const res = calculate(genCalc, atk, defender, calcMv, field);
|
|
629
|
+
const r = res.range();
|
|
630
|
+
if (r[1] < defender.maxHP()) {
|
|
631
|
+
if (best === null || hp + d < best.total) {
|
|
632
|
+
best = { total: hp + d, hp, d };
|
|
633
|
+
range = r;
|
|
634
|
+
hp0 = defender.maxHP();
|
|
635
|
+
}
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (best === null) {
|
|
641
|
+
throw new Error(`Cannot survive ${args.survive.attacker.species} ${args.survive.move} even with 252 HP / 252 ${defStat.toUpperCase()}.`);
|
|
642
|
+
}
|
|
643
|
+
required.hp = Math.max(required.hp, best.hp);
|
|
644
|
+
required[defStat] = Math.max(required[defStat], best.d);
|
|
645
|
+
verification.push(`survive: ${args.survive.attacker.species} ${args.survive.move} -> ${range[0]}-${range[1]} vs ${hp0} HP (max ${Math.round((range[1] / hp0) * 100)}%)`);
|
|
646
|
+
}
|
|
647
|
+
if (args.outspeed) {
|
|
648
|
+
let targetSpeed;
|
|
649
|
+
if (args.outspeed.target) {
|
|
650
|
+
requireExists(dex.species.get(args.outspeed.target.species), 'Pokemon species', args.outspeed.target.species);
|
|
651
|
+
targetSpeed = buildPokemon(gen, args.outspeed.target).stats.spe;
|
|
652
|
+
}
|
|
653
|
+
else if (args.outspeed.speed) {
|
|
654
|
+
targetSpeed = args.outspeed.speed;
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
throw new Error('outspeed requires `target` or `speed`.');
|
|
658
|
+
}
|
|
659
|
+
let minEv = -1;
|
|
660
|
+
for (let e = 0; e <= 252; e += 4) {
|
|
661
|
+
if (finalStat(gen, 'spe', sp.baseStats.spe, ivs.spe, e, args.level, nature) > targetSpeed) {
|
|
662
|
+
minEv = e;
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (minEv < 0)
|
|
667
|
+
throw new Error(`Cannot outspeed ${targetSpeed} with this nature/IV.`);
|
|
668
|
+
required.spe = Math.max(required.spe, minEv);
|
|
669
|
+
verification.push(`outspeed: ${minEv} Speed EVs -> ${finalStat(gen, 'spe', sp.baseStats.spe, ivs.spe, minEv, args.level, nature)} > ${targetSpeed}`);
|
|
670
|
+
}
|
|
671
|
+
if (args.kill) {
|
|
672
|
+
const mv = dex.moves.get(args.kill.move);
|
|
673
|
+
requireExists(mv, 'move', args.kill.move);
|
|
674
|
+
const target = buildPokemon(gen, args.kill.target);
|
|
675
|
+
const calcMv = new CalcMove(genCalc, args.kill.move);
|
|
676
|
+
const offStat = mv.category === 'Special' ? 'spa' : 'atk';
|
|
677
|
+
const needPerHit = Math.ceil(target.maxHP() / args.kill.hits);
|
|
678
|
+
let minEv = -1;
|
|
679
|
+
let range = [0, 0];
|
|
680
|
+
for (let e = 0; e <= 252; e += 4) {
|
|
681
|
+
const attacker = build({ [offStat]: e });
|
|
682
|
+
const res = calculate(genCalc, attacker, target, calcMv, field);
|
|
683
|
+
const r = res.range();
|
|
684
|
+
if (r[0] >= needPerHit) {
|
|
685
|
+
minEv = e;
|
|
686
|
+
range = r;
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (minEv < 0)
|
|
691
|
+
throw new Error(`Cannot guarantee a ${args.kill.hits}-hit KO on ${args.kill.target.species}.`);
|
|
692
|
+
required[offStat] = Math.max(required[offStat], minEv);
|
|
693
|
+
verification.push(`kill: ${minEv} ${offStat.toUpperCase()} EVs -> ${range[0]}-${range[1]} vs ${target.maxHP()} HP (min ${Math.round((range[0] / target.maxHP()) * 100)}%)`);
|
|
694
|
+
}
|
|
695
|
+
const used = STATS.reduce((s, st) => s + (required[st] ?? 0), 0);
|
|
696
|
+
const remaining = Math.max(0, 510 - used);
|
|
697
|
+
const add = Math.min(252 - (required[args.maximize] ?? 0), Math.floor(remaining / 4) * 4);
|
|
698
|
+
required[args.maximize] = (required[args.maximize] ?? 0) + add;
|
|
699
|
+
const finalStats = {};
|
|
700
|
+
for (const st of STATS) {
|
|
701
|
+
finalStats[st] = finalStat(gen, st, sp.baseStats[st], ivs[st], required[st], args.level, nature);
|
|
702
|
+
}
|
|
703
|
+
return ok({
|
|
704
|
+
species: sp.name,
|
|
705
|
+
generation: gen,
|
|
706
|
+
level: args.level,
|
|
707
|
+
nature,
|
|
708
|
+
item: args.item,
|
|
709
|
+
evs: required,
|
|
710
|
+
stats: finalStats,
|
|
711
|
+
totalEVs: STATS.reduce((s, st) => s + required[st], 0),
|
|
712
|
+
unusedEVs: Math.max(0, 508 - STATS.reduce((s, st) => s + required[st], 0)),
|
|
713
|
+
verification,
|
|
714
|
+
note: 'EVs are computed in steps of 4 (508 usable of 510). The maximize stat is capped at 252; unused EVs can be reallocated manually.',
|
|
715
|
+
});
|
|
716
|
+
}));
|
|
717
|
+
}
|
|
718
|
+
//# sourceMappingURL=calc.js.map
|