koffing 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -7
- package/dist/data-CP51LAc9.cjs +404 -0
- package/dist/data-CRHBhbCJ.js +345 -0
- package/dist/index.cjs +441 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +27 -104
- package/dist/index.js +432 -430
- package/dist/types-BVzP_Ko4.d.cts +62 -0
- package/dist/types-BVzP_Ko4.d.ts +62 -0
- package/dist/validator.cjs +3563 -0
- package/dist/validator.d.cts +14 -0
- package/dist/validator.d.ts +14 -0
- package/dist/validator.js +3562 -0
- package/package.json +43 -22
- package/dist/index.mjs +0 -400
package/README.md
CHANGED
|
@@ -1,13 +1,100 @@
|
|
|
1
1
|
# Koffing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A dependency-free Pokémon Showdown parser and formatter for JavaScript and TypeScript. Supports ESM and CommonJS. The API uses plain data and pure functions; it deliberately replaces the previous classes and `name`/`nickname` schema.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```sh
|
|
6
|
+
pnpm add koffing
|
|
7
|
+
```
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
```ts
|
|
10
|
+
import { parse, parseJSON, exportTeam, exportTeams } from "koffing";
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
const result = parse("Smogon (Koffing) @ Eviolite\nAbility: Levitate\n- Sludge Bomb");
|
|
13
|
+
console.log(result.diagnostics);
|
|
14
|
+
const sets = result.teams[0]!.pokemon;
|
|
15
|
+
// [{ species: "Koffing", name: "Smogon", item: "Eviolite", ... }]
|
|
16
|
+
const json = JSON.stringify(sets, null, 2); // Showdown-shaped set array
|
|
17
|
+
const imported = parseJSON(json);
|
|
18
|
+
const showdown = exportTeam(sets); // No team metadata header
|
|
19
|
+
const backup = exportTeams(imported.teams); // Preserves named team metadata
|
|
20
|
+
```
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
## Parsing and diagnostics
|
|
23
|
+
|
|
24
|
+
`parse(text, options?)` returns `{ teams, diagnostics }`. Each team contains `pokemon: PokemonSet[]` and optional `name`, `format`, and `folder`. Headerless input does not invent metadata. `parseJSON(value, options?)` accepts JSON text or plain data: a single set, an array of sets, a `{ pokemon, ...metadata }` team, or a `{ teams }` collection.
|
|
25
|
+
|
|
26
|
+
Set fields follow Showdown: required `species` and `moves`, optional `name` for a nickname, plus `item`, `ability`, `gender`, `nature`, `evs`, `ivs`, `level`, `shiny`, `happiness`, `pokeball`, `hpType`, `dynamaxLevel`, `gigantamax`, and `teraType`. Stat tables can be sparse.
|
|
27
|
+
|
|
28
|
+
Supported text includes nicknames, genders, item headers, team backups, `Trait`/`Friendship`/`Ball` aliases, `Item:`/`Nickname:`/`Species:`/`Move:`, bracketed abilities, bare `Shiny`/`Gigantamax`, and EV nature modifiers. Hidden Power moves use the internal `Hidden Power Ice` form with `hpType`, and export with brackets. Explicit happiness and Hidden Power fields take precedence over inferred values. Unknown species and move names are retained without database lookup.
|
|
29
|
+
|
|
30
|
+
Permissive mode is the default. Diagnostics identify unknown or malformed lines, duplicate fields, and suspicious numeric values; text diagnostics include one-based line numbers. Supported values are preserved, including levels over 100 and moves beyond four. Strict mode rejects these diagnostics. This is semantic preservation, not a byte-for-byte document editor: whitespace is normalized, repeated fields use the last accepted value, and unsupported lines are omitted with diagnostics. Always inspect diagnostics before replacing the original input.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { parse, KoffingError } from "koffing";
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
parse(input, { mode: "strict" });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error instanceof KoffingError) console.error(error.diagnostics);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Strict mode throws on parsing diagnostics. Unsafe JSON structures, resource-limit violations, and packed team strings fail in either parsing mode. Packed formats are not interpreted as Pokémon names. Use upstream Showdown for pack/unpack support.
|
|
43
|
+
|
|
44
|
+
## Parser scope and safety
|
|
45
|
+
|
|
46
|
+
Koffing retains generic sanity diagnostics for traditional numeric ranges, EV totals, and move counts. These parser checks do not use a game database. Species, moves, abilities, items, natures, balls, and types remain open strings when parsing. New text syntax or fields may still require parser support. The optional `koffing/validator` entry point adds snapshot-based identifier checks; learnsets and format legality remain outside its scope.
|
|
47
|
+
|
|
48
|
+
### Optional identifier and basic legality validation
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { parse } from "koffing";
|
|
52
|
+
import { validate } from "koffing/validator";
|
|
53
|
+
|
|
54
|
+
const parsed = parse("Koffing @ Eviolite\nAbility: Levitate\n- Sludge Bomb");
|
|
55
|
+
const result = validate({ teams: parsed.teams });
|
|
56
|
+
console.log(result.valid, result.diagnostics);
|
|
57
|
+
// Also accepts a set, set array, team wrapper, or JSON string, like parseJSON.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This separate entry point keeps lookup data out of the core parser bundle. `validate(input, options?)` accepts unknown data without coercion or mutation and returns `{ valid, diagnostics }`. It checks runtime field types, known fields, finite integers, level 1–100, happiness 0–255, Dynamax level 0–10, IVs 0–31, EVs 0–255 with a 510 total, one to six Pokémon per team, and one to four distinct moves. EVs retain the historical 255 cap because no generation is assumed. Missing optional fields and empty optional strings remain unspecified. Shape failures return the first error; other checks can return multiple errors. Strict mode throws on diagnostics; resource-limit violations always throw. Inspect parser diagnostics separately when validating parsed text.
|
|
61
|
+
|
|
62
|
+
Species, moves, items, abilities, natures, balls, Hidden Power types, and Tera types are checked against committed JSON tables. Display names and lowercase alphanumeric IDs compare equivalently; aliases are not resolved. Balls must be ball items; Hidden Power excludes Normal, Fairy, and Stellar. Typed Hidden Power variants count as one move for duplicate checks. Snapshot identifiers include past and nonstandard Showdown entries. A passing result is only a basic sanity check: it does not prove obtainability, species/ability compatibility, learnsets, event restrictions, generation availability, battle-form eligibility, or format legality. Species/item clauses and generation-specific EV rules are intentionally not imposed.
|
|
63
|
+
|
|
64
|
+
Run `pnpm --filter koffing lookups:generate` to dump sorted, deduplicated IDs from the lockfile-pinned development-only `pokemon-showdown` dependency into `src/lookups/*.json`; `source.json` records its version and scope. `pnpm --filter koffing lookups:check` detects stale tables without writing. Review and commit regenerated files when updating Showdown. Runtime validation has no Showdown dependency or network requests.
|
|
65
|
+
|
|
66
|
+
### Validation and explicit sanitization
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { validateTeam, sanitizeTeam } from "koffing";
|
|
70
|
+
|
|
71
|
+
const issues = validateTeam(sets);
|
|
72
|
+
const { pokemon, diagnostics } = sanitizeTeam(sets, {
|
|
73
|
+
maxLevel: 100,
|
|
74
|
+
maxMovesPerPokemon: 4,
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`validateTeam` reports generic numeric ranges, move counts, and EV totals. `sanitizeTeam` clones input, clamps supported numeric fields and truncates moves only when explicitly called, and reports changes. EVs use a 510-point budget allocated in HP, Atk, Def, SpA, SpD, Spe order, capped at 255 per stat. It is not a competitive legality validator. Sanitizing does not establish legality or choose an optimal EV distribution.
|
|
79
|
+
|
|
80
|
+
`exportTeam(pokemon)` and `exportTeams(teams)` follow Showdown's trusted-object approach: they read typed data directly without cloning, descriptor inspection, unknown-property checks, diagnostics, or resource budgets. They accept no parsing options. Extra properties are ignored and nonfinite numbers are omitted; finite values, explicit defaults, and extra moves are preserved. For untrusted input, call `parseJSON` or `validateTeam` explicitly before export. Export does not escape delimiters or control characters, so callers are responsible for supplying representable text fields; arbitrary strings need not round-trip. Exports are Showdown text, not HTML.
|
|
81
|
+
|
|
82
|
+
## Resource limits
|
|
83
|
+
|
|
84
|
+
Parsing and validation entry points accept `limits` overrides. Defaults are 2,000,000 UTF-16 input code units, 16,384 code units per line, 1,000 teams, 10,000 Pokémon in total, 256 moves per Pokémon, and 1,000 diagnostics. Limits are resource budgets, not battle rules. Exceeding a limit throws; it never returns a silently truncated collection. Overrides must be positive safe integers.
|
|
85
|
+
|
|
86
|
+
For object inputs to parsing and validation, the input budget conservatively counts known string lengths plus 32 units per string and 512 per set. Small custom budgets may therefore reject objects whose compact JSON would fit. Export does not impose input or output budgets.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
parse(input, { limits: { maxInputLength: 100_000, maxPokemon: 60 } });
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
JSON parsing and validation accept ordinary data objects and reject accessor properties and non-plain records. Export reads properties directly, including getters. JavaScript proxies are executable objects and should not be treated as untrusted JSON; accept JSON text at trust boundaries.
|
|
93
|
+
|
|
94
|
+
## Compatibility and verification
|
|
95
|
+
|
|
96
|
+
The compatibility target is Showdown's exported text and sparse `PokemonSet` shape. Koffing retains backup metadata separately. Formatting need not be byte-identical to Showdown: explicit defaults can remain visible and harmless whitespace differs. Species aliases, generation-specific defaults, Hidden Power IV inference, and legality checks require game data and are intentionally not inferred by the core. Call a game-data resolver or Showdown validator separately when needed.
|
|
97
|
+
|
|
98
|
+
Differential tests compare supported semantic behavior with the official server implementation. The development-only reference version and source commit are recorded in `test/upstream.json`, and `pnpm-lock.yaml` fixes its installation. This dependency also supplies the optional validator's generated ID tables. Updating the reference requires reviewing its expectations and regenerating those tables. Additional tests cover client syntax, old fixtures, malformed inputs, limits, serialization boundaries, optional validation, and round trips. There are no web app tests.
|
|
99
|
+
|
|
100
|
+
Run `pnpm test` for tests and `pnpm bench` for Vitest benchmarks. Benchmarks cover normal teams, large backups, and malformed long lines; they are observations rather than CI speed thresholds. Upstream and Koffing perform different validation and metadata work, so throughput is not a like-for-like measure of every feature.
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
//#region src/limits.ts
|
|
2
|
+
const DEFAULT_LIMITS = Object.freeze({
|
|
3
|
+
maxInputLength: 2e6,
|
|
4
|
+
maxLineLength: 16384,
|
|
5
|
+
maxTeams: 1e3,
|
|
6
|
+
maxPokemon: 1e4,
|
|
7
|
+
maxMoves: 256,
|
|
8
|
+
maxDiagnostics: 1e3
|
|
9
|
+
});
|
|
10
|
+
var KoffingError = class extends Error {
|
|
11
|
+
diagnostics;
|
|
12
|
+
constructor(message, diagnostics) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "KoffingError";
|
|
15
|
+
this.diagnostics = diagnostics;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function resolveLimits(options = {}) {
|
|
19
|
+
if (options.mode !== void 0 && options.mode !== "strict" && options.mode !== "permissive") throw new TypeError("mode must be strict or permissive");
|
|
20
|
+
if (options.limits === void 0) return DEFAULT_LIMITS;
|
|
21
|
+
const limits = {
|
|
22
|
+
...DEFAULT_LIMITS,
|
|
23
|
+
...options.limits
|
|
24
|
+
};
|
|
25
|
+
for (const [key, value] of Object.entries(limits)) if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${key} must be a positive safe integer`);
|
|
26
|
+
return limits;
|
|
27
|
+
}
|
|
28
|
+
function fail(code, message, line) {
|
|
29
|
+
throw new KoffingError(message, [{
|
|
30
|
+
code,
|
|
31
|
+
message,
|
|
32
|
+
severity: "error",
|
|
33
|
+
...line === void 0 ? {} : { line }
|
|
34
|
+
}]);
|
|
35
|
+
}
|
|
36
|
+
function checkInput(input, limits) {
|
|
37
|
+
if (typeof input !== "string") throw new TypeError("Input must be a string");
|
|
38
|
+
if (input.length > limits.maxInputLength) fail("input-limit", `Input exceeds ${limits.maxInputLength} code units`);
|
|
39
|
+
}
|
|
40
|
+
function finish(diagnostics, options) {
|
|
41
|
+
if (options.mode === "strict" && diagnostics.length) throw new KoffingError(diagnostics[0].message, diagnostics);
|
|
42
|
+
}
|
|
43
|
+
function report(diagnostics, diagnostic, limits) {
|
|
44
|
+
if (diagnostics.length >= limits.maxDiagnostics) fail("diagnostic-limit", "Too many parsing issues", diagnostic.line);
|
|
45
|
+
diagnostics.push(diagnostic);
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/data.ts
|
|
49
|
+
const strings = [
|
|
50
|
+
"species",
|
|
51
|
+
"name",
|
|
52
|
+
"item",
|
|
53
|
+
"ability",
|
|
54
|
+
"nature",
|
|
55
|
+
"pokeball",
|
|
56
|
+
"hpType",
|
|
57
|
+
"teraType"
|
|
58
|
+
];
|
|
59
|
+
const numbers = [
|
|
60
|
+
"level",
|
|
61
|
+
"happiness",
|
|
62
|
+
"dynamaxLevel"
|
|
63
|
+
];
|
|
64
|
+
const flags = ["shiny", "gigantamax"];
|
|
65
|
+
const stats = [
|
|
66
|
+
"hp",
|
|
67
|
+
"atk",
|
|
68
|
+
"def",
|
|
69
|
+
"spa",
|
|
70
|
+
"spd",
|
|
71
|
+
"spe"
|
|
72
|
+
];
|
|
73
|
+
const statKeys = new Set(stats);
|
|
74
|
+
const teamKeys = /* @__PURE__ */ new Set([
|
|
75
|
+
"name",
|
|
76
|
+
"format",
|
|
77
|
+
"folder",
|
|
78
|
+
"pokemon"
|
|
79
|
+
]);
|
|
80
|
+
const collectionKeys = /* @__PURE__ */ new Set(["teams"]);
|
|
81
|
+
const setKeys = /* @__PURE__ */ new Set([
|
|
82
|
+
...strings,
|
|
83
|
+
...numbers,
|
|
84
|
+
...flags,
|
|
85
|
+
"gender",
|
|
86
|
+
"evs",
|
|
87
|
+
"ivs",
|
|
88
|
+
"moves"
|
|
89
|
+
]);
|
|
90
|
+
function objectKeys(value, path, limits) {
|
|
91
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) fail("invalid-json", `${path} must be a plain object`);
|
|
92
|
+
const keys = Reflect.ownKeys(value);
|
|
93
|
+
if (keys.length > limits.maxDiagnostics + 32) fail("property-limit", `${path} contains too many properties`);
|
|
94
|
+
return keys;
|
|
95
|
+
}
|
|
96
|
+
function property(value, key, path, limits) {
|
|
97
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
98
|
+
if (typeof key !== "string" || !("value" in descriptor)) fail("invalid-json", `${path} contains an accessor or symbol`);
|
|
99
|
+
if (key.length > Math.max(32, limits.maxLineLength)) fail("property-limit", `${path} contains an oversized property name`);
|
|
100
|
+
return descriptor.value;
|
|
101
|
+
}
|
|
102
|
+
function object(value, path, limits) {
|
|
103
|
+
const keys = objectKeys(value, path, limits);
|
|
104
|
+
const result = Object.create(null);
|
|
105
|
+
for (const key of keys) result[key] = property(value, key, path, limits);
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
function array(value, path, maximum) {
|
|
109
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) fail("invalid-json", `${path} must be an array`);
|
|
110
|
+
if (value.length > maximum) fail("collection-limit", `${path} exceeds ${maximum} entries`);
|
|
111
|
+
if (Reflect.ownKeys(value).length !== value.length + 1) fail("invalid-json", `${path} contains non-index properties or holes`);
|
|
112
|
+
const result = [];
|
|
113
|
+
for (let i = 0; i < value.length; i++) {
|
|
114
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
|
|
115
|
+
if (!descriptor || !("value" in descriptor)) fail("invalid-json", `${path} must not contain holes or accessors`);
|
|
116
|
+
result.push(descriptor.value);
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function text(value, path, limits, budget) {
|
|
121
|
+
if (typeof value !== "string" || /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(value) || value !== value.trim()) fail("invalid-text", `${path} must be trimmed text without control characters`);
|
|
122
|
+
if (value.length > limits.maxLineLength) fail("line-limit", `${path} exceeds ${limits.maxLineLength} code units`);
|
|
123
|
+
budget.used += value.length + 32;
|
|
124
|
+
if (budget.used > limits.maxInputLength) fail("input-limit", "Aggregate data exceeds configured input limit");
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
function finite(value, path) {
|
|
128
|
+
if (typeof value !== "number" || !Number.isFinite(value)) fail("invalid-number", `${path} must be finite`);
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
function unknownKeys(source, known, path, diagnostics, limits) {
|
|
132
|
+
for (const key of Object.keys(source)) if (!known.has(key)) report(diagnostics, {
|
|
133
|
+
code: "unknown-field",
|
|
134
|
+
severity: "warning",
|
|
135
|
+
path: `${path}.${key}`,
|
|
136
|
+
message: `Unknown field ${path}.${key}`
|
|
137
|
+
}, limits);
|
|
138
|
+
}
|
|
139
|
+
function readSet(value, path, diagnostics, limits, budget) {
|
|
140
|
+
const keys = objectKeys(value, path, limits);
|
|
141
|
+
budget.used += 512;
|
|
142
|
+
if (budget.used > limits.maxInputLength) fail("input-limit", "Aggregate data exceeds configured input limit");
|
|
143
|
+
const result = {
|
|
144
|
+
species: "",
|
|
145
|
+
moves: []
|
|
146
|
+
};
|
|
147
|
+
let hasMoves = false;
|
|
148
|
+
for (const rawKey of keys) {
|
|
149
|
+
const entry = property(value, rawKey, path, limits);
|
|
150
|
+
const key = rawKey;
|
|
151
|
+
if (!setKeys.has(key)) {
|
|
152
|
+
report(diagnostics, {
|
|
153
|
+
code: "unknown-field",
|
|
154
|
+
severity: "warning",
|
|
155
|
+
path: `${path}.${key}`,
|
|
156
|
+
message: `Unknown field ${path}.${key}`
|
|
157
|
+
}, limits);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (entry === void 0 && key !== "species" && key !== "moves") continue;
|
|
161
|
+
switch (key) {
|
|
162
|
+
case "species":
|
|
163
|
+
case "name":
|
|
164
|
+
case "item":
|
|
165
|
+
case "ability":
|
|
166
|
+
case "nature":
|
|
167
|
+
case "pokeball":
|
|
168
|
+
case "hpType":
|
|
169
|
+
case "teraType":
|
|
170
|
+
result[key] = text(entry, `${path}.${key}`, limits, budget);
|
|
171
|
+
break;
|
|
172
|
+
case "level":
|
|
173
|
+
case "happiness":
|
|
174
|
+
case "dynamaxLevel":
|
|
175
|
+
result[key] = finite(entry, `${path}.${key}`);
|
|
176
|
+
break;
|
|
177
|
+
case "shiny":
|
|
178
|
+
case "gigantamax":
|
|
179
|
+
if (typeof entry !== "boolean") fail("invalid-boolean", `${path}.${key} must be boolean`);
|
|
180
|
+
result[key] = entry;
|
|
181
|
+
break;
|
|
182
|
+
case "gender":
|
|
183
|
+
if (entry !== "M" && entry !== "F" && entry !== "N" && entry !== "") fail("invalid-gender", `${path}.gender must be M, F, N or empty`);
|
|
184
|
+
result.gender = entry;
|
|
185
|
+
break;
|
|
186
|
+
case "evs":
|
|
187
|
+
case "ivs": {
|
|
188
|
+
const statPath = `${path}.${key}`;
|
|
189
|
+
const values = {};
|
|
190
|
+
for (const rawStat of objectKeys(entry, statPath, limits)) {
|
|
191
|
+
const number = property(entry, rawStat, statPath, limits);
|
|
192
|
+
const stat = rawStat;
|
|
193
|
+
if (!statKeys.has(stat)) report(diagnostics, {
|
|
194
|
+
code: "unknown-field",
|
|
195
|
+
severity: "warning",
|
|
196
|
+
path: `${statPath}.${stat}`,
|
|
197
|
+
message: `Unknown field ${statPath}.${stat}`
|
|
198
|
+
}, limits);
|
|
199
|
+
else if (number !== void 0) values[stat] = finite(number, `${statPath}.${stat}`);
|
|
200
|
+
}
|
|
201
|
+
result[key] = values;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "moves": {
|
|
205
|
+
hasMoves = true;
|
|
206
|
+
const moves = array(entry, `${path}.moves`, limits.maxMoves);
|
|
207
|
+
for (let index = 0; index < moves.length; index++) if (!text(moves[index], `${path}.moves[${index}]`, limits, budget)) fail("invalid-move", `${path}.moves[${index}] cannot be empty`);
|
|
208
|
+
result.moves = moves;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (!result.species) fail("invalid-species", `${path}.species is required`);
|
|
214
|
+
if (!hasMoves) fail("invalid-json", `${path}.moves must be an array`);
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
/** Decode and copy known data only. Shape errors are fatal; unknown fields are diagnosed. */
|
|
218
|
+
function parseJSON(input, options = {}) {
|
|
219
|
+
const limits = resolveLimits(options);
|
|
220
|
+
let value = input;
|
|
221
|
+
if (typeof value === "string") {
|
|
222
|
+
checkInput(value, limits);
|
|
223
|
+
try {
|
|
224
|
+
value = JSON.parse(value);
|
|
225
|
+
} catch {
|
|
226
|
+
fail("invalid-json", "Invalid JSON syntax");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const diagnostics = [];
|
|
230
|
+
const budget = { used: 0 };
|
|
231
|
+
let rawTeams;
|
|
232
|
+
if (Array.isArray(value)) rawTeams = [{ pokemon: value }];
|
|
233
|
+
else {
|
|
234
|
+
const source = object(value, "input", limits);
|
|
235
|
+
if (Object.hasOwn(source, "teams")) {
|
|
236
|
+
unknownKeys(source, collectionKeys, "input", diagnostics, limits);
|
|
237
|
+
rawTeams = array(source.teams, "teams", limits.maxTeams);
|
|
238
|
+
} else if (Object.hasOwn(source, "pokemon")) rawTeams = [source];
|
|
239
|
+
else rawTeams = [{ pokemon: [source] }];
|
|
240
|
+
}
|
|
241
|
+
if (rawTeams.length > limits.maxTeams) fail("collection-limit", "Too many teams");
|
|
242
|
+
let total = 0;
|
|
243
|
+
const teams = rawTeams.map((raw, index) => {
|
|
244
|
+
const path = `teams[${index}]`;
|
|
245
|
+
const source = object(raw, path, limits);
|
|
246
|
+
unknownKeys(source, teamKeys, path, diagnostics, limits);
|
|
247
|
+
const members = array(source.pokemon, `${path}.pokemon`, limits.maxPokemon - total);
|
|
248
|
+
total += members.length;
|
|
249
|
+
const team = { pokemon: members.map((member, i) => readSet(member, `${path}.pokemon[${i}]`, diagnostics, limits, budget)) };
|
|
250
|
+
for (const key of [
|
|
251
|
+
"name",
|
|
252
|
+
"format",
|
|
253
|
+
"folder"
|
|
254
|
+
]) if (source[key] !== void 0) team[key] = text(source[key], `${path}.${key}`, limits, budget);
|
|
255
|
+
return team;
|
|
256
|
+
});
|
|
257
|
+
for (const [index, team] of teams.entries()) checkRanges(team.pokemon, `teams[${index}].pokemon`, diagnostics, limits);
|
|
258
|
+
finish(diagnostics, options);
|
|
259
|
+
return {
|
|
260
|
+
teams,
|
|
261
|
+
diagnostics
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/** Generic traditional-format checks, not species or format legality validation. */
|
|
265
|
+
function checkRanges(pokemon, path, diagnostics, limits) {
|
|
266
|
+
const add = (code, path, message) => report(diagnostics, {
|
|
267
|
+
code,
|
|
268
|
+
severity: "warning",
|
|
269
|
+
path,
|
|
270
|
+
message
|
|
271
|
+
}, limits);
|
|
272
|
+
for (const [i, set] of pokemon.entries()) {
|
|
273
|
+
const prefix = `${path}[${i}]`;
|
|
274
|
+
const range = (value, min, max, field, code = "number-range") => {
|
|
275
|
+
if (value !== void 0 && (!Number.isInteger(value) || value < min || value > max)) add(code, `${prefix}.${field}`, `Expected an integer from ${min} to ${max}; value preserved`);
|
|
276
|
+
};
|
|
277
|
+
range(set.level, 1, 100, "level");
|
|
278
|
+
range(set.happiness, 0, 255, "happiness");
|
|
279
|
+
range(set.dynamaxLevel, 0, 10, "dynamaxLevel");
|
|
280
|
+
let evTotal = 0;
|
|
281
|
+
for (const stat of stats) {
|
|
282
|
+
const ev = set.evs?.[stat];
|
|
283
|
+
const iv = set.ivs?.[stat];
|
|
284
|
+
if (ev !== void 0) {
|
|
285
|
+
evTotal += ev;
|
|
286
|
+
range(ev, 0, 255, `evs.${stat}`, "stat-range");
|
|
287
|
+
}
|
|
288
|
+
if (iv !== void 0) range(iv, 0, 31, `ivs.${stat}`, "stat-range");
|
|
289
|
+
}
|
|
290
|
+
if (evTotal > 510) add("ev-total", `${prefix}.evs`, "Traditional EV total exceeds 510");
|
|
291
|
+
if (set.moves.length > 4) add("move-count", `${prefix}.moves`, "Traditional sets contain at most four moves");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function validateTeam(pokemon, options = {}) {
|
|
295
|
+
return parseJSON(pokemon, options).diagnostics;
|
|
296
|
+
}
|
|
297
|
+
/** Explicit, deterministic traditional-range sanitization; never mutates its input. */
|
|
298
|
+
function sanitizeTeam(pokemon, options = {}) {
|
|
299
|
+
const maxLevel = options.maxLevel ?? 100;
|
|
300
|
+
const maxMoves = options.maxMovesPerPokemon ?? 4;
|
|
301
|
+
if (!Number.isSafeInteger(maxLevel) || maxLevel < 1 || !Number.isSafeInteger(maxMoves) || maxMoves < 0) throw new TypeError("Sanitization limits must be valid nonnegative integers (maxLevel >= 1)");
|
|
302
|
+
const parsed = parseJSON(pokemon, {
|
|
303
|
+
...options,
|
|
304
|
+
mode: "permissive"
|
|
305
|
+
});
|
|
306
|
+
const result = parsed.teams[0].pokemon;
|
|
307
|
+
const diagnostics = parsed.diagnostics;
|
|
308
|
+
const limits = resolveLimits(options);
|
|
309
|
+
const changed = (path) => report(diagnostics, {
|
|
310
|
+
code: "sanitized",
|
|
311
|
+
severity: "warning",
|
|
312
|
+
path,
|
|
313
|
+
message: `Sanitized ${path}`
|
|
314
|
+
}, limits);
|
|
315
|
+
const clamp = (value, min, max, path) => {
|
|
316
|
+
const next = Math.max(min, Math.min(max, Math.trunc(value)));
|
|
317
|
+
if (next !== value) changed(path);
|
|
318
|
+
return next;
|
|
319
|
+
};
|
|
320
|
+
for (const [i, set] of result.entries()) {
|
|
321
|
+
const path = `pokemon[${i}]`;
|
|
322
|
+
if (set.level !== void 0) set.level = clamp(set.level, 1, maxLevel, `${path}.level`);
|
|
323
|
+
if (set.happiness !== void 0) set.happiness = clamp(set.happiness, 0, 255, `${path}.happiness`);
|
|
324
|
+
if (set.dynamaxLevel !== void 0) set.dynamaxLevel = clamp(set.dynamaxLevel, 0, 10, `${path}.dynamaxLevel`);
|
|
325
|
+
let evBudget = 510;
|
|
326
|
+
for (const stat of stats) {
|
|
327
|
+
if (set.evs?.[stat] !== void 0) {
|
|
328
|
+
set.evs[stat] = clamp(set.evs[stat], 0, Math.min(255, evBudget), `${path}.evs.${stat}`);
|
|
329
|
+
evBudget -= set.evs[stat];
|
|
330
|
+
}
|
|
331
|
+
if (set.ivs?.[stat] !== void 0) set.ivs[stat] = clamp(set.ivs[stat], 0, 31, `${path}.ivs.${stat}`);
|
|
332
|
+
}
|
|
333
|
+
if (set.moves.length > maxMoves) {
|
|
334
|
+
set.moves = set.moves.slice(0, maxMoves);
|
|
335
|
+
changed(`${path}.moves`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
finish(diagnostics, options);
|
|
339
|
+
return {
|
|
340
|
+
pokemon: result,
|
|
341
|
+
diagnostics
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
Object.defineProperty(exports, "DEFAULT_LIMITS", {
|
|
346
|
+
enumerable: true,
|
|
347
|
+
get: function() {
|
|
348
|
+
return DEFAULT_LIMITS;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
Object.defineProperty(exports, "KoffingError", {
|
|
352
|
+
enumerable: true,
|
|
353
|
+
get: function() {
|
|
354
|
+
return KoffingError;
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
Object.defineProperty(exports, "checkInput", {
|
|
358
|
+
enumerable: true,
|
|
359
|
+
get: function() {
|
|
360
|
+
return checkInput;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
Object.defineProperty(exports, "fail", {
|
|
364
|
+
enumerable: true,
|
|
365
|
+
get: function() {
|
|
366
|
+
return fail;
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
Object.defineProperty(exports, "finish", {
|
|
370
|
+
enumerable: true,
|
|
371
|
+
get: function() {
|
|
372
|
+
return finish;
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
Object.defineProperty(exports, "parseJSON", {
|
|
376
|
+
enumerable: true,
|
|
377
|
+
get: function() {
|
|
378
|
+
return parseJSON;
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
Object.defineProperty(exports, "report", {
|
|
382
|
+
enumerable: true,
|
|
383
|
+
get: function() {
|
|
384
|
+
return report;
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
Object.defineProperty(exports, "resolveLimits", {
|
|
388
|
+
enumerable: true,
|
|
389
|
+
get: function() {
|
|
390
|
+
return resolveLimits;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
Object.defineProperty(exports, "sanitizeTeam", {
|
|
394
|
+
enumerable: true,
|
|
395
|
+
get: function() {
|
|
396
|
+
return sanitizeTeam;
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
Object.defineProperty(exports, "validateTeam", {
|
|
400
|
+
enumerable: true,
|
|
401
|
+
get: function() {
|
|
402
|
+
return validateTeam;
|
|
403
|
+
}
|
|
404
|
+
});
|