Compare commits
14 Commits
bf1793e561
...
70141b162f
Author | SHA1 | Date | |
---|---|---|---|
70141b162f | |||
6d92cb4deb | |||
32e142b8cb | |||
c6249b1b9b | |||
4f1e84ba8e | |||
225cac02c0 | |||
a1f2f175cd | |||
4b07aeb10c | |||
699a833285 | |||
648deb83db | |||
a926461f8f | |||
a65137307b | |||
0afca583b0 | |||
dc668c67a0 |
145
.gitignore
vendored
145
.gitignore
vendored
@ -11,3 +11,148 @@ htmlcov
|
||||
*.db
|
||||
cache/
|
||||
token
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node
|
||||
|
28
bot.py
28
bot.py
@ -61,11 +61,37 @@ class BotClient(discord.Client):
|
||||
|
||||
async def on_message(self, message: discord.Message):
|
||||
content = message.content
|
||||
if self.is_replay(message):
|
||||
if self.is_command(message):
|
||||
await self.on_command(message)
|
||||
elif self.is_replay(message):
|
||||
await self.on_replay(message)
|
||||
elif self.is_leaguefact(message):
|
||||
await self.on_leaguefact(message)
|
||||
|
||||
def is_command(self, message: discord.Message) -> bool:
|
||||
return message.content.startswith("%")
|
||||
|
||||
async def on_command(self, message: discord.Message):
|
||||
match message.content.split(" "):
|
||||
case ["%calc", *args]:
|
||||
try:
|
||||
calc = await self._calculate(args)
|
||||
await message.reply(content=calc.strip())
|
||||
except Exception as e:
|
||||
await message.reply(
|
||||
content="```\n" + str(e) + "\n```", delete_after=30
|
||||
)
|
||||
case _:
|
||||
_log.info(f"Unrecognised command {command}")
|
||||
|
||||
async def _calculate(self, args: list[str]) -> str:
|
||||
proc = sp.run(
|
||||
["node", "calc_main.js", "--"] + args, stdout=sp.PIPE, stderr=sp.PIPE
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise Exception(proc.stderr.decode())
|
||||
return proc.stdout.decode()
|
||||
|
||||
def is_replay(self, message: discord.Message) -> bool:
|
||||
if re.match("https://replay.pokemonshowdown.com/dl-.*", message.content):
|
||||
return True
|
||||
|
304
calc.js
Normal file
304
calc.js
Normal file
@ -0,0 +1,304 @@
|
||||
import {
|
||||
calculate,
|
||||
Field,
|
||||
Generations,
|
||||
Move,
|
||||
Pokemon,
|
||||
Result,
|
||||
} from "@ajhyndman/smogon-calc";
|
||||
import assert from "assert";
|
||||
import { createToken, Lexer } from "chevrotain";
|
||||
|
||||
const gen = Generations.get(9);
|
||||
|
||||
/**
|
||||
* Creates a lexer.
|
||||
*
|
||||
* @returns {Lexer}
|
||||
*/
|
||||
function buildLexer() {
|
||||
const Boost = createToken({ name: "Boost", pattern: /[+-]\d+/ });
|
||||
const EV = createToken({ name: "EV", pattern: /\d+[+-]?/ });
|
||||
const Stat = createToken({
|
||||
name: "Stat",
|
||||
pattern: /HP|Atk|Def|SpA|SpD|Spe/,
|
||||
});
|
||||
|
||||
const Item = createToken({
|
||||
name: "Item",
|
||||
pattern: new RegExp([...gen.items].map((i) => i.name).join("|")),
|
||||
});
|
||||
const Ability = createToken({
|
||||
name: "Ability",
|
||||
pattern: new RegExp([...gen.abilities].map((a) => a.name).join("|")),
|
||||
});
|
||||
|
||||
const Pokemon = createToken({
|
||||
name: "Pokemon",
|
||||
pattern: new RegExp([...gen.species].map((s) => s.name).join("|")),
|
||||
});
|
||||
const Move = createToken({
|
||||
name: "Move",
|
||||
pattern: new RegExp([...gen.moves].map((m) => m.name).join("|")),
|
||||
});
|
||||
|
||||
const whitespace = createToken({
|
||||
name: "Whitespace",
|
||||
pattern: /\s+/,
|
||||
group: Lexer.SKIPPED,
|
||||
});
|
||||
const div = createToken({
|
||||
name: "div",
|
||||
pattern: "/",
|
||||
group: Lexer.SKIPPED,
|
||||
});
|
||||
const vs = createToken({
|
||||
name: "vs",
|
||||
pattern: /vs\.?/,
|
||||
});
|
||||
|
||||
const terrainEnter = createToken({
|
||||
name: "terrainEnter",
|
||||
pattern: "in",
|
||||
push_mode: "terrain_mode",
|
||||
});
|
||||
const Weather = createToken({
|
||||
name: "Weather",
|
||||
pattern: /Sun|Rain|Sand|Snow/,
|
||||
});
|
||||
const Terrain = createToken({
|
||||
name: "Terrain",
|
||||
pattern: /(Electric|Grassy|Misty|Psychic) Terrain/,
|
||||
});
|
||||
|
||||
const screenEnter = createToken({
|
||||
name: "screenEnter",
|
||||
pattern: "through",
|
||||
push_mode: "screen_mode",
|
||||
});
|
||||
|
||||
return new Lexer({
|
||||
modes: {
|
||||
default_mode: [
|
||||
whitespace,
|
||||
div,
|
||||
vs,
|
||||
Boost,
|
||||
EV,
|
||||
Stat,
|
||||
Item,
|
||||
Ability,
|
||||
Pokemon,
|
||||
Move,
|
||||
terrainEnter,
|
||||
screenEnter,
|
||||
],
|
||||
terrain_mode: [whitespace, screenEnter, Weather, Terrain],
|
||||
screen_mode: [whitespace, Move],
|
||||
},
|
||||
defaultMode: "default_mode",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @generator
|
||||
* @template T item type
|
||||
* @param {T[]} arr
|
||||
* @yields {T} item
|
||||
*/
|
||||
function* iterate(arr) {
|
||||
for (const a of arr) {
|
||||
yield a;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type token type
|
||||
* @param {import("chevrotain").IToken} token token
|
||||
* @return {string} matched token
|
||||
*/
|
||||
function unwrapToken(type, token) {
|
||||
assert(
|
||||
token.tokenType.name == type,
|
||||
"expected token %s, got %s",
|
||||
type,
|
||||
token.tokenType.name
|
||||
);
|
||||
return token.image;
|
||||
}
|
||||
|
||||
const POS_NATURES = {
|
||||
atk: "Adamant",
|
||||
def: "Bold",
|
||||
spa: "Modest",
|
||||
spd: "Calm",
|
||||
spe: "Jolly",
|
||||
};
|
||||
|
||||
const NEG_NATURES = {
|
||||
atk: "Modest",
|
||||
def: "Lonely",
|
||||
spa: "Adamant",
|
||||
spd: "Rash",
|
||||
spe: "Brave",
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a Smogon calculator output:
|
||||
*
|
||||
* -2 8 SpA Choice Specs Torkoal vs.
|
||||
* 252 HP / 4+ SpD Assault Vest Abomasnow
|
||||
* in Sun
|
||||
* through Light Screen
|
||||
*
|
||||
* @param {string} line textual line
|
||||
* @return {Result} calculation result
|
||||
*/
|
||||
function parseAndCalculate(line) {
|
||||
const lexer = buildLexer();
|
||||
const result = lexer.tokenize(line);
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
console.error("Unparsed tokens: %o", result.errors);
|
||||
}
|
||||
|
||||
/** @type {string} */
|
||||
var attacker;
|
||||
/** @type {string} */
|
||||
var defender;
|
||||
|
||||
/** @type {import("@ajhyndman/smogon-calc").State.Pokemon} */
|
||||
var attackerOpts = {};
|
||||
/** @type {import("@ajhyndman/smogon-calc").State.Pokemon} */
|
||||
var defenderOpts = {};
|
||||
|
||||
/** @type {string} */
|
||||
var move;
|
||||
/** @type {import("@ajhyndman/smogon-calc").State.Field} */
|
||||
var field = {};
|
||||
|
||||
// Tokenising state.
|
||||
var isAttacker = true;
|
||||
var it = iterate(result.tokens);
|
||||
|
||||
const opts = () => (isAttacker ? attackerOpts : defenderOpts);
|
||||
|
||||
while (true) {
|
||||
let item = it.next().value;
|
||||
if (!item) break;
|
||||
switch (item.tokenType.name) {
|
||||
case "Boost":
|
||||
{
|
||||
let boost = unwrapToken("Boost", item);
|
||||
let ev = unwrapToken("EV", it.next().value);
|
||||
let stat = unwrapToken("Stat", it.next().value).toLowerCase();
|
||||
opts().boosts = { [stat]: parseInt(boost), ...opts().boosts };
|
||||
opts().evs = { [stat]: parseInt(ev), ...opts().evs };
|
||||
if (ev.endsWith("+")) {
|
||||
opts().nature = POS_NATURES[stat];
|
||||
} else if (ev.endsWith("-")) {
|
||||
opts().nature = NEG_NATURES[stat];
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "EV":
|
||||
{
|
||||
let ev = unwrapToken("EV", item);
|
||||
let stat = unwrapToken("Stat", it.next().value).toLowerCase();
|
||||
opts().evs = { [stat]: parseInt(ev), ...opts().evs };
|
||||
if (ev.endsWith("+")) {
|
||||
opts().nature = POS_NATURES[stat];
|
||||
} else if (ev.endsWith("-")) {
|
||||
opts().nature = NEG_NATURES[stat];
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "Stat":
|
||||
throw Error("Impossible state: bare Stat");
|
||||
case "Item":
|
||||
opts().item = unwrapToken("Item", item);
|
||||
break;
|
||||
case "Pokemon":
|
||||
{
|
||||
let name = unwrapToken("Pokemon", item);
|
||||
if (isAttacker) attacker = name;
|
||||
else defender = name;
|
||||
}
|
||||
break;
|
||||
case "Move":
|
||||
move = unwrapToken("Move", item);
|
||||
break;
|
||||
case "terrainEnter":
|
||||
{
|
||||
let next = it.next().value;
|
||||
switch (next.tokenType.name) {
|
||||
case "Weather":
|
||||
field.weather = next.image;
|
||||
break;
|
||||
case "Terrain":
|
||||
field.terrain = next.image;
|
||||
break;
|
||||
default:
|
||||
throw Error(
|
||||
"Unhandled terrain %s: %s",
|
||||
next.tokenType.name,
|
||||
next.image
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "screenEnter":
|
||||
{
|
||||
let screen = unwrapToken("Move", it.next().value);
|
||||
field.defenderSide = field.defenderSide || {};
|
||||
field.defenderSide.isLightScreen = screen === "Light Screen";
|
||||
field.defenderSide.isReflect = screen === "Reflect";
|
||||
field.defenderSide.isAuroraVeil = screen === "Aurora Veil";
|
||||
field.defenderSide.is;
|
||||
}
|
||||
break;
|
||||
case "vs":
|
||||
isAttacker = false;
|
||||
break;
|
||||
default:
|
||||
console.error("unmatched token type: %s", item.tokenType.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-checking before the calculator throws unreadable errors.
|
||||
if (!gen.species.get(attacker.toLowerCase()))
|
||||
throw Error(`No species named ${attacker}`);
|
||||
if (!gen.species.get(defender.toLowerCase()))
|
||||
throw Error(`No species named ${defender}`);
|
||||
if (!gen.moves.get(move.toLowerCase())) throw Error(`No move named ${move}`);
|
||||
|
||||
return calculate(
|
||||
gen,
|
||||
new Pokemon(gen, attacker, attackerOpts),
|
||||
new Pokemon(gen, defender, defenderOpts),
|
||||
new Move(gen, move),
|
||||
new Field(field)
|
||||
);
|
||||
}
|
||||
|
||||
function test() {
|
||||
const text =
|
||||
"-2 8 SpA Choice Specs Torkoal Overheat vs. 252 HP / 4+ SpD Assault Vest Abomasnow in Sun through Light Screen";
|
||||
const res = parseAndCalculate(text);
|
||||
|
||||
assert(res.attacker.boosts.spa === -2, "should have -2 SpA");
|
||||
assert(res.attacker.evs.spa === 8, "should have 8 SpA EVs");
|
||||
assert(res.attacker.item === "Choice Specs", "should have Choice Specs");
|
||||
assert(res.attacker.name === "Torkoal", "should be Torkoal");
|
||||
assert(res.move.name === "Overheat", "should be Overheat");
|
||||
assert(res.defender.evs.hp === 252, "should have 252 HP EVs");
|
||||
assert(res.defender.evs.spd === 4, "should have 4 SpD EVs");
|
||||
assert(res.field.weather === "Sun", "should be in sun");
|
||||
|
||||
assert(
|
||||
res.desc().replace(/:.*/, "") === text,
|
||||
"non-damage text should be equivalent to input"
|
||||
);
|
||||
}
|
||||
|
||||
export { parseAndCalculate, test };
|
8
calc_main.js
Normal file
8
calc_main.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { parseAndCalculate } from "./calc.js";
|
||||
|
||||
// FIXME: issue with using execArgv.
|
||||
let args = process.argv.slice(2);
|
||||
if (args[0] === "--") args = args.slice(1);
|
||||
const line = args.join(" ");
|
||||
const res = parseAndCalculate(line);
|
||||
console.log(res.fullDesc());
|
@ -17,7 +17,11 @@
|
||||
ps.requests
|
||||
]);
|
||||
in
|
||||
[ python pkgs.sqlite ];
|
||||
[
|
||||
pkgs.nodejs
|
||||
python
|
||||
pkgs.sqlite
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
||||
|
82
package-lock.json
generated
Normal file
82
package-lock.json
generated
Normal file
@ -0,0 +1,82 @@
|
||||
{
|
||||
"name": "hhirls",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hhirls",
|
||||
"version": "1.0.0",
|
||||
"license": "WTFPL",
|
||||
"dependencies": {
|
||||
"@ajhyndman/smogon-calc": "^0.8.0",
|
||||
"chevrotain": "^10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ajhyndman/smogon-calc": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@ajhyndman/smogon-calc/-/smogon-calc-0.8.0.tgz",
|
||||
"integrity": "sha512-jx/gY1uSD70skzE0xE7+lUPp7KxKKqj3PcyRLMCejbd9A68PwnBKfbDP3pW5ILX9Dy+9R9m6OkiPCa9K4qi/pQ==",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@chevrotain/cst-dts-gen": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz",
|
||||
"integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==",
|
||||
"dependencies": {
|
||||
"@chevrotain/gast": "10.5.0",
|
||||
"@chevrotain/types": "10.5.0",
|
||||
"lodash": "4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@chevrotain/gast": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz",
|
||||
"integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==",
|
||||
"dependencies": {
|
||||
"@chevrotain/types": "10.5.0",
|
||||
"lodash": "4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@chevrotain/types": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz",
|
||||
"integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A=="
|
||||
},
|
||||
"node_modules/@chevrotain/utils": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz",
|
||||
"integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.1.tgz",
|
||||
"integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA=="
|
||||
},
|
||||
"node_modules/chevrotain": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz",
|
||||
"integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==",
|
||||
"dependencies": {
|
||||
"@chevrotain/cst-dts-gen": "10.5.0",
|
||||
"@chevrotain/gast": "10.5.0",
|
||||
"@chevrotain/types": "10.5.0",
|
||||
"@chevrotain/utils": "10.5.0",
|
||||
"lodash": "4.17.21",
|
||||
"regexp-to-ast": "0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/regexp-to-ast": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz",
|
||||
"integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="
|
||||
}
|
||||
}
|
||||
}
|
17
package.json
Normal file
17
package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "hhirls",
|
||||
"version": "1.0.0",
|
||||
"description": "Pokemon Showdown data processing, mostly for HHIRLLL's Pokemon league. Ugly as fuck.",
|
||||
"main": "calc.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"calc": "node calc_main.js",
|
||||
"test": "node -e 'import(\"./calc.js\").then(mod => mod.test())'"
|
||||
},
|
||||
"author": "",
|
||||
"license": "WTFPL",
|
||||
"dependencies": {
|
||||
"@ajhyndman/smogon-calc": "^0.8.0",
|
||||
"chevrotain": "^10.5.0"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user