diff --git a/config.example.json b/config.example.json index acfbde2..c2414c7 100644 --- a/config.example.json +++ b/config.example.json @@ -1,3 +1,7 @@ { - "token": "token" + "token": "token", + "sitePort": 15385, + "siteURL": "http://localhost:15385", + "clientId": "", + "clientSecret": "" } \ No newline at end of file diff --git a/deploy-commands.cjs b/deploy-commands.cjs index f7e04a0..7053d6d 100644 --- a/deploy-commands.cjs +++ b/deploy-commands.cjs @@ -17,7 +17,7 @@ rest const commandFiles = fs.readdirSync("./dist/commands").filter((file) => file.endsWith(".js") && !file.startsWith('.')); for (const file of commandFiles) { - const command = require(`./dist/commands/${file}`); + const command = require(`./dist/commands/${file}`).default; commands.push(command); } diff --git a/migrations/20231113151937_giveCountersIds.js b/migrations/20231113151937_giveCountersIds.js index a1424e2..6161b0e 100644 --- a/migrations/20231113151937_giveCountersIds.js +++ b/migrations/20231113151937_giveCountersIds.js @@ -19,12 +19,12 @@ exports.up = async function(knex) { // awfulllllllllllllll const rows = await knex('counters').select('*'); - await knex('counters_').insert(rows); + if (rows.length > 0) await knex('counters_').insert(rows); await knex.schema .dropTable('counters') .renameTable('counters_', 'counters'); - + await knex.schema .alterTable('counterUserLink', table => { table.integer('id').references('id').inTable('counters'); diff --git a/migrations/20231114153325_items.js b/migrations/20231114153325_items.js new file mode 100644 index 0000000..98ef104 --- /dev/null +++ b/migrations/20231114153325_items.js @@ -0,0 +1,34 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema + .createTable('customItems', table => { + table.increments('id'); + table.string('guild').notNullable(); + table.string('name').notNullable(); + table.text('description'); + table.string('emoji').notNullable(); + table.enum('type', ['plain', 'weapon', 'consumable']).notNullable(); + table.integer('maxStack').notNullable(); // or damage for weapons + table.string('behavior'); + table.boolean('untradable').defaultTo(false); + table.float('behaviorValue'); + }) + .createTable('itemInventories', table => { + table.string('user').notNullable(); + table.integer('item').notNullable(); + table.integer('quantity').defaultTo(1); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema + .dropTable('customItems') + .dropTable('itemInventories'); +}; diff --git a/migrations/20231115005045_counterLinks.js b/migrations/20231115005045_counterLinks.js new file mode 100644 index 0000000..17887f3 --- /dev/null +++ b/migrations/20231115005045_counterLinks.js @@ -0,0 +1,21 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema + .alterTable('counters', table => { + table.integer('linkedItem'); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema + .alterTable('counters', table => { + table.dropColumn('linkedItem'); + }); +}; diff --git a/migrations/20231115140015_craftingStationCooldowns.js b/migrations/20231115140015_craftingStationCooldowns.js new file mode 100644 index 0000000..b170c23 --- /dev/null +++ b/migrations/20231115140015_craftingStationCooldowns.js @@ -0,0 +1,21 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema + .createTable('craftingStationCooldowns', table => { + table.string('station').notNullable(); + table.string('user').notNullable(); + table.timestamp('usedAt').notNullable(); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema + .dropTable('craftingStationCooldowns'); +}; diff --git a/migrations/20231117173052_craftingRecipes.js b/migrations/20231117173052_craftingRecipes.js new file mode 100644 index 0000000..66a37e7 --- /dev/null +++ b/migrations/20231117173052_craftingRecipes.js @@ -0,0 +1,28 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema + .createTable('customCraftingRecipes', (table) => { + table.increments('id'); + table.string('guild'); + table.string('station'); + }) + .createTable('customCraftingRecipeItems', (table) => { + table.integer('id').references('id').inTable('customCraftingRecipes').notNullable(); + table.integer('item').notNullable(); + table.integer('quantity').defaultTo(1); + table.enum('type', ['input', 'output', 'requirement']); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema + .dropTable('customCraftingRecipes') + .dropTable('customCraftingRecipeItems'); +}; diff --git a/migrations/20231119183807_sessions.js b/migrations/20231119183807_sessions.js new file mode 100644 index 0000000..584f1c6 --- /dev/null +++ b/migrations/20231119183807_sessions.js @@ -0,0 +1,23 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema + .createTable('sessions', table => { + table.string('id').notNullable(); + table.string('tokenType').notNullable(); + table.string('accessToken').notNullable(); + table.string('refreshToken').notNullable(); + table.timestamp('expiresAt').notNullable(); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema + .dropTable('sessions'); +}; diff --git a/migrations/20231121082135_initHealth.js b/migrations/20231121082135_initHealth.js new file mode 100644 index 0000000..876d373 --- /dev/null +++ b/migrations/20231121082135_initHealth.js @@ -0,0 +1,19 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema + .createTable('initHealth', table => + table.string('user').notNullable().unique() + ); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema + .dropTable('initHealth'); +}; diff --git a/migrations/20231121194328_invincible.js b/migrations/20231121194328_invincible.js new file mode 100644 index 0000000..a672087 --- /dev/null +++ b/migrations/20231121194328_invincible.js @@ -0,0 +1,20 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema + .createTable('invincibleUsers', table => { + table.string('user').notNullable().unique(); + table.timestamp('since').notNullable(); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema + .dropTable('invincibleUsers'); +}; diff --git a/migrations/20231121203119_behaviors.js b/migrations/20231121203119_behaviors.js new file mode 100644 index 0000000..8f55873 --- /dev/null +++ b/migrations/20231121203119_behaviors.js @@ -0,0 +1,26 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema + .createTable('itemBehaviors', table => { + table.integer('item').notNullable(); + table.string('behavior').notNullable(); + table.float('value'); + }) + .alterTable('customItems', table => { + table.dropColumn('behavior'); + table.dropColumn('behaviorValue'); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +exports.down = function(knex) { + // no + throw 'Not implemented'; +}; diff --git a/package.json b/package.json index a266572..2645282 100644 --- a/package.json +++ b/package.json @@ -12,24 +12,33 @@ "author": "oatmealine", "license": "AGPL-3.0", "dependencies": { + "@discordjs/core": "^1.1.1", + "@discordjs/rest": "^2.2.0", "chalk": "^4.1.2", "d3-array": "^2.12.1", - "discord.js": "^14.13.0", + "discord.js": "^14.14.1", + "express": "^4.18.2", + "express-handlebars": "^7.1.2", "got": "^11.8.6", "knex": "^3.0.1", "outdent": "^0.8.0", "parse-color": "^1.0.0", "pretty-bytes": "^5.6.0", "random-seed": "^0.3.0", - "sqlite3": "^5.1.6" + "sqlite3": "^5.1.6", + "tough-cookie": "^4.1.3", + "uid-safe": "^2.1.5" }, "devDependencies": { - "@types/d3-array": "^3.0.9", - "@types/parse-color": "^1.0.2", - "@typescript-eslint/eslint-plugin": "^6.9.0", - "@typescript-eslint/parser": "^6.9.0", - "discord-api-types": "^0.37.50", - "eslint": "^8.52.0", + "@types/d3-array": "^3.2.1", + "@types/express": "^4.17.21", + "@types/parse-color": "^1.0.3", + "@types/tough-cookie": "^4.0.5", + "@types/uid-safe": "^2.1.5", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "discord-api-types": "^0.37.63", + "eslint": "^8.53.0", "typescript": "5.2.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 046356a..4087693 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,12 @@ settings: excludeLinksFromLockfile: false dependencies: + '@discordjs/core': + specifier: ^1.1.1 + version: 1.1.1 + '@discordjs/rest': + specifier: ^2.2.0 + version: 2.2.0 chalk: specifier: ^4.1.2 version: 4.1.2 @@ -12,8 +18,14 @@ dependencies: specifier: ^2.12.1 version: 2.12.1 discord.js: - specifier: ^14.13.0 - version: 14.13.0 + specifier: ^14.14.1 + version: 14.14.1 + express: + specifier: ^4.18.2 + version: 4.18.2 + express-handlebars: + specifier: ^7.1.2 + version: 7.1.2 got: specifier: ^11.8.6 version: 11.8.6 @@ -35,26 +47,41 @@ dependencies: sqlite3: specifier: ^5.1.6 version: 5.1.6 + tough-cookie: + specifier: ^4.1.3 + version: 4.1.3 + uid-safe: + specifier: ^2.1.5 + version: 2.1.5 devDependencies: '@types/d3-array': - specifier: ^3.0.9 - version: 3.0.9 + specifier: ^3.2.1 + version: 3.2.1 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 '@types/parse-color': - specifier: ^1.0.2 - version: 1.0.2 + specifier: ^1.0.3 + version: 1.0.3 + '@types/tough-cookie': + specifier: ^4.0.5 + version: 4.0.5 + '@types/uid-safe': + specifier: ^2.1.5 + version: 2.1.5 '@typescript-eslint/eslint-plugin': - specifier: ^6.9.0 - version: 6.9.0(@typescript-eslint/parser@6.9.0)(eslint@8.52.0)(typescript@5.2.2) + specifier: ^6.11.0 + version: 6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.2.2) '@typescript-eslint/parser': - specifier: ^6.9.0 - version: 6.9.0(eslint@8.52.0)(typescript@5.2.2) + specifier: ^6.11.0 + version: 6.11.0(eslint@8.53.0)(typescript@5.2.2) discord-api-types: - specifier: ^0.37.50 - version: 0.37.50 + specifier: ^0.37.63 + version: 0.37.63 eslint: - specifier: ^8.52.0 - version: 8.52.0 + specifier: ^8.53.0 + version: 8.53.0 typescript: specifier: 5.2.2 version: 5.2.2 @@ -66,14 +93,14 @@ packages: engines: {node: '>=0.10.0'} dev: true - /@discordjs/builders@1.6.5: - resolution: {integrity: sha512-SdweyCs/+mHj+PNhGLLle7RrRFX9ZAhzynHahMCLqp5Zeq7np7XC6/mgzHc79QoVlQ1zZtOkTTiJpOZu5V8Ufg==} + /@discordjs/builders@1.7.0: + resolution: {integrity: sha512-GDtbKMkg433cOZur8Dv6c25EHxduNIBsxeHrsRoIM8+AwmEZ8r0tEpckx/sHwTLwQPOF3e2JWloZh9ofCaMfAw==} engines: {node: '>=16.11.0'} dependencies: - '@discordjs/formatters': 0.3.2 - '@discordjs/util': 1.0.1 + '@discordjs/formatters': 0.3.3 + '@discordjs/util': 1.0.2 '@sapphire/shapeshift': 3.9.3 - discord-api-types: 0.37.50 + discord-api-types: 0.37.61 fast-deep-equal: 3.1.3 ts-mixer: 6.0.3 tslib: 2.6.2 @@ -84,44 +111,64 @@ packages: engines: {node: '>=16.11.0'} dev: false - /@discordjs/formatters@0.3.2: - resolution: {integrity: sha512-lE++JZK8LSSDRM5nLjhuvWhGuKiXqu+JZ/DsOR89DVVia3z9fdCJVcHF2W/1Zxgq0re7kCzmAJlCMMX3tetKpA==} - engines: {node: '>=16.11.0'} - dependencies: - discord-api-types: 0.37.50 + /@discordjs/collection@2.0.0: + resolution: {integrity: sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==} + engines: {node: '>=18'} dev: false - /@discordjs/rest@2.0.1: - resolution: {integrity: sha512-/eWAdDRvwX/rIE2tuQUmKaxmWeHmGealttIzGzlYfI4+a7y9b6ZoMp8BG/jaohs8D8iEnCNYaZiOFLVFLQb8Zg==} + /@discordjs/core@1.1.1: + resolution: {integrity: sha512-3tDqc6KCAtE0CxNl5300uPzFnNsY/GAmJhc6oGutbl/la+4mRv5zVb4N68cmcaeD2Il/ySH4zIc00sq+cyhtUA==} + engines: {node: '>=18'} + dependencies: + '@discordjs/rest': 2.2.0 + '@discordjs/util': 1.0.2 + '@discordjs/ws': 1.0.2 + '@sapphire/snowflake': 3.5.1 + '@vladfrangu/async_event_emitter': 2.2.2 + discord-api-types: 0.37.61 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /@discordjs/formatters@0.3.3: + resolution: {integrity: sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==} engines: {node: '>=16.11.0'} dependencies: - '@discordjs/collection': 1.5.3 - '@discordjs/util': 1.0.1 + discord-api-types: 0.37.61 + dev: false + + /@discordjs/rest@2.2.0: + resolution: {integrity: sha512-nXm9wT8oqrYFRMEqTXQx9DUTeEtXUDMmnUKIhZn6O2EeDY9VCdwj23XCPq7fkqMPKdF7ldAfeVKyxxFdbZl59A==} + engines: {node: '>=16.11.0'} + dependencies: + '@discordjs/collection': 2.0.0 + '@discordjs/util': 1.0.2 '@sapphire/async-queue': 1.5.0 '@sapphire/snowflake': 3.5.1 '@vladfrangu/async_event_emitter': 2.2.2 - discord-api-types: 0.37.50 + discord-api-types: 0.37.61 magic-bytes.js: 1.5.0 tslib: 2.6.2 - undici: 5.22.1 + undici: 5.27.2 dev: false - /@discordjs/util@1.0.1: - resolution: {integrity: sha512-d0N2yCxB8r4bn00/hvFZwM7goDcUhtViC5un4hPj73Ba4yrChLSJD8fy7Ps5jpTLg1fE9n4K0xBLc1y9WGwSsA==} + /@discordjs/util@1.0.2: + resolution: {integrity: sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==} engines: {node: '>=16.11.0'} dev: false - /@discordjs/ws@1.0.1: - resolution: {integrity: sha512-avvAolBqN3yrSvdBPcJ/0j2g42ABzrv3PEL76e3YTp2WYMGH7cuspkjfSyNWaqYl1J+669dlLp+YFMxSVQyS5g==} + /@discordjs/ws@1.0.2: + resolution: {integrity: sha512-+XI82Rm2hKnFwAySXEep4A7Kfoowt6weO6381jgW+wVdTpMS/56qCvoXyFRY0slcv7c/U8My2PwIB2/wEaAh7Q==} engines: {node: '>=16.11.0'} dependencies: - '@discordjs/collection': 1.5.3 - '@discordjs/rest': 2.0.1 - '@discordjs/util': 1.0.1 + '@discordjs/collection': 2.0.0 + '@discordjs/rest': 2.2.0 + '@discordjs/util': 1.0.2 '@sapphire/async-queue': 1.5.0 - '@types/ws': 8.5.8 + '@types/ws': 8.5.9 '@vladfrangu/async_event_emitter': 2.2.2 - discord-api-types: 0.37.50 + discord-api-types: 0.37.61 tslib: 2.6.2 ws: 8.14.2 transitivePeerDependencies: @@ -129,13 +176,13 @@ packages: - utf-8-validate dev: false - /@eslint-community/eslint-utils@4.4.0(eslint@8.52.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.52.0 + eslint: 8.53.0 eslint-visitor-keys: 3.4.3 dev: true @@ -144,8 +191,8 @@ packages: engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true - /@eslint/eslintrc@2.1.2: - resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} + /@eslint/eslintrc@2.1.3: + resolution: {integrity: sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 @@ -161,11 +208,16 @@ packages: - supports-color dev: true - /@eslint/js@8.52.0: - resolution: {integrity: sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==} + /@eslint/js@8.53.0: + resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@fastify/busboy@2.1.0: + resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==} + engines: {node: '>=14'} + dev: false + /@gar/promisify@1.1.3: resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} requiresBuild: true @@ -192,6 +244,18 @@ packages: resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} dev: true + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: false + /@mapbox/node-pre-gyp@1.0.11: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true @@ -251,6 +315,13 @@ packages: dev: false optional: true + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: false + optional: true + /@sapphire/async-queue@1.5.0: resolution: {integrity: sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} @@ -288,61 +359,134 @@ packages: dev: false optional: true + /@types/body-parser@1.19.5: + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.9.0 + dev: true + /@types/cacheable-request@6.0.3: resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} dependencies: - '@types/http-cache-semantics': 4.0.3 + '@types/http-cache-semantics': 4.0.4 '@types/keyv': 3.1.4 - '@types/node': 20.8.9 - '@types/responselike': 1.0.2 + '@types/node': 20.9.0 + '@types/responselike': 1.0.3 dev: false - /@types/d3-array@3.0.9: - resolution: {integrity: sha512-mZowFN3p64ajCJJ4riVYlOjNlBJv3hctgAY01pjw3qTnJePD8s9DZmYDzhHKvzfCYvdjwylkU38+Vdt7Cu2FDA==} + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + dependencies: + '@types/node': 20.9.0 dev: true - /@types/http-cache-semantics@4.0.3: - resolution: {integrity: sha512-V46MYLFp08Wf2mmaBhvgjStM3tPa+2GAdy/iqoX+noX1//zje2x4XmrIU0cAwyClATsTmahbtoQ2EwP7I5WSiA==} + /@types/d3-array@3.2.1: + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + dev: true + + /@types/express-serve-static-core@4.17.41: + resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==} + dependencies: + '@types/node': 20.9.0 + '@types/qs': 6.9.10 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + dev: true + + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.17.41 + '@types/qs': 6.9.10 + '@types/serve-static': 1.15.5 + dev: true + + /@types/http-cache-semantics@4.0.4: + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} dev: false - /@types/json-schema@7.0.14: - resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==} + /@types/http-errors@2.0.4: + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + dev: true + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 20.8.9 + '@types/node': 20.9.0 dev: false - /@types/node@20.8.9: - resolution: {integrity: sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==} + /@types/mime@1.3.5: + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + dev: true + + /@types/mime@3.0.4: + resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} + dev: true + + /@types/node@20.9.0: + resolution: {integrity: sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==} dependencies: undici-types: 5.26.5 - dev: false - /@types/parse-color@1.0.2: - resolution: {integrity: sha512-Bx+cf6G8qAD7Ks1uAdoF7KkeqZZdHBoJrQXddbLaqBynAtHQsMEI/A8u9MpkOvedloHAqk9+dyxgCUmPNjVz/Q==} + /@types/parse-color@1.0.3: + resolution: {integrity: sha512-1zpnjrXCl0KklQOtMDDXhqQN9ouePkt4NBoAJ/dRjyMqWMkegyIqeZUOf3Xq4yaUPPdY8wmR8cye/D9v1kcrsQ==} dev: true - /@types/responselike@1.0.2: - resolution: {integrity: sha512-/4YQT5Kp6HxUDb4yhRkm0bJ7TbjvTddqX7PZ5hz6qV3pxSo72f/6YPRo+Mu2DU307tm9IioO69l7uAwn5XNcFA==} - dependencies: - '@types/node': 20.8.9 - dev: false - - /@types/semver@7.5.4: - resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==} + /@types/qs@6.9.10: + resolution: {integrity: sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==} dev: true - /@types/ws@8.5.8: - resolution: {integrity: sha512-flUksGIQCnJd6sZ1l5dqCEG/ksaoAg/eUwiLAGTJQcfgvZJKF++Ta4bJA6A5aPSJmsr+xlseHn4KLgVlNnvPTg==} + /@types/range-parser@1.2.7: + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + dev: true + + /@types/responselike@1.0.3: + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} dependencies: - '@types/node': 20.8.9 + '@types/node': 20.9.0 dev: false - /@typescript-eslint/eslint-plugin@6.9.0(@typescript-eslint/parser@6.9.0)(eslint@8.52.0)(typescript@5.2.2): - resolution: {integrity: sha512-lgX7F0azQwRPB7t7WAyeHWVfW1YJ9NIgd9mvGhfQpRY56X6AVf8mwM8Wol+0z4liE7XX3QOt8MN1rUKCfSjRIA==} + /@types/semver@7.5.5: + resolution: {integrity: sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==} + dev: true + + /@types/send@0.17.4: + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.9.0 + dev: true + + /@types/serve-static@1.15.5: + resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==} + dependencies: + '@types/http-errors': 2.0.4 + '@types/mime': 3.0.4 + '@types/node': 20.9.0 + dev: true + + /@types/tough-cookie@4.0.5: + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + dev: true + + /@types/uid-safe@2.1.5: + resolution: {integrity: sha512-RwEfbxqXKEay2b5p8QQVllfnMbVPUZChiKKZ2M6+OSRRmvr4HTCCUZTWhr/QlmrMnNE0ViNBBbP1+5plF9OGRw==} + dev: true + + /@types/ws@8.5.9: + resolution: {integrity: sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==} + dependencies: + '@types/node': 20.9.0 + dev: false + + /@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha @@ -353,13 +497,13 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.9.0(eslint@8.52.0)(typescript@5.2.2) - '@typescript-eslint/scope-manager': 6.9.0 - '@typescript-eslint/type-utils': 6.9.0(eslint@8.52.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.9.0(eslint@8.52.0)(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.9.0 + '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 6.11.0 + '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.11.0 debug: 4.3.4 - eslint: 8.52.0 + eslint: 8.53.0 graphemer: 1.4.0 ignore: 5.2.4 natural-compare: 1.4.0 @@ -370,8 +514,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.9.0(eslint@8.52.0)(typescript@5.2.2): - resolution: {integrity: sha512-GZmjMh4AJ/5gaH4XF2eXA8tMnHWP+Pm1mjQR2QN4Iz+j/zO04b9TOvJYOX2sCNIQHtRStKTxRY1FX7LhpJT4Gw==} + /@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -380,27 +524,27 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.9.0 - '@typescript-eslint/types': 6.9.0 - '@typescript-eslint/typescript-estree': 6.9.0(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.9.0 + '@typescript-eslint/scope-manager': 6.11.0 + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.11.0 debug: 4.3.4 - eslint: 8.52.0 + eslint: 8.53.0 typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/scope-manager@6.9.0: - resolution: {integrity: sha512-1R8A9Mc39n4pCCz9o79qRO31HGNDvC7UhPhv26TovDsWPBDx+Sg3rOZdCELIA3ZmNoWAuxaMOT7aWtGRSYkQxw==} + /@typescript-eslint/scope-manager@6.11.0: + resolution: {integrity: sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.9.0 - '@typescript-eslint/visitor-keys': 6.9.0 + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/visitor-keys': 6.11.0 dev: true - /@typescript-eslint/type-utils@6.9.0(eslint@8.52.0)(typescript@5.2.2): - resolution: {integrity: sha512-XXeahmfbpuhVbhSOROIzJ+b13krFmgtc4GlEuu1WBT+RpyGPIA4Y/eGnXzjbDj5gZLzpAXO/sj+IF/x2GtTMjQ==} + /@typescript-eslint/type-utils@6.11.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -409,23 +553,23 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.9.0(typescript@5.2.2) - '@typescript-eslint/utils': 6.9.0(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.2.2) + '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.2.2) debug: 4.3.4 - eslint: 8.52.0 + eslint: 8.53.0 ts-api-utils: 1.0.3(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/types@6.9.0: - resolution: {integrity: sha512-+KB0lbkpxBkBSiVCuQvduqMJy+I1FyDbdwSpM3IoBS7APl4Bu15lStPjgBIdykdRqQNYqYNMa8Kuidax6phaEw==} + /@typescript-eslint/types@6.11.0: + resolution: {integrity: sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==} engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@6.9.0(typescript@5.2.2): - resolution: {integrity: sha512-NJM2BnJFZBEAbCfBP00zONKXvMqihZCrmwCaik0UhLr0vAgb6oguXxLX1k00oQyD+vZZ+CJn3kocvv2yxm4awQ==} + /@typescript-eslint/typescript-estree@6.11.0(typescript@5.2.2): + resolution: {integrity: sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -433,8 +577,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.9.0 - '@typescript-eslint/visitor-keys': 6.9.0 + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/visitor-keys': 6.11.0 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 @@ -445,30 +589,30 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@6.9.0(eslint@8.52.0)(typescript@5.2.2): - resolution: {integrity: sha512-5Wf+Jsqya7WcCO8me504FBigeQKVLAMPmUzYgDbWchINNh1KJbxCgVya3EQ2MjvJMVeXl3pofRmprqX6mfQkjQ==} + /@typescript-eslint/utils@6.11.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) - '@types/json-schema': 7.0.14 - '@types/semver': 7.5.4 - '@typescript-eslint/scope-manager': 6.9.0 - '@typescript-eslint/types': 6.9.0 - '@typescript-eslint/typescript-estree': 6.9.0(typescript@5.2.2) - eslint: 8.52.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.5 + '@typescript-eslint/scope-manager': 6.11.0 + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.2.2) + eslint: 8.53.0 semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/visitor-keys@6.9.0: - resolution: {integrity: sha512-dGtAfqjV6RFOtIP8I0B4ZTBRrlTT8NHHlZZSchQx3qReaoDeXhYM++M4So2AgFK9ZB0emRPA6JI1HkafzA2Ibg==} + /@typescript-eslint/visitor-keys@6.11.0: + resolution: {integrity: sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.9.0 + '@typescript-eslint/types': 6.11.0 eslint-visitor-keys: 3.4.3 dev: true @@ -486,6 +630,14 @@ packages: requiresBuild: true dev: false + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + /acorn-jsx@5.3.2(acorn@8.11.2): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -541,12 +693,22 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: false + /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} dependencies: color-convert: 2.0.1 + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: false + /aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} dev: false @@ -573,6 +735,10 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + /array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -581,12 +747,38 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: false + /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} engines: {node: '>=8'} @@ -594,11 +786,9 @@ packages: fill-range: 7.0.1 dev: true - /busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - dependencies: - streamsearch: 1.1.0 + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} dev: false /cacache@15.3.0: @@ -647,6 +837,14 @@ packages: responselike: 2.0.1 dev: false + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 + dev: false + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -711,6 +909,27 @@ packages: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} dev: false + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -718,7 +937,6 @@ packages: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - dev: true /d3-array@2.12.1: resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} @@ -726,6 +944,17 @@ packages: internmap: 1.0.1 dev: false + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -753,10 +982,29 @@ packages: engines: {node: '>=10'} dev: false + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: false + /delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + /detect-libc@2.0.2: resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} engines: {node: '>=8'} @@ -769,26 +1017,31 @@ packages: path-type: 4.0.0 dev: true - /discord-api-types@0.37.50: - resolution: {integrity: sha512-X4CDiMnDbA3s3RaUXWXmgAIbY1uxab3fqe3qwzg5XutR3wjqi7M3IkgQbsIBzpqBN2YWr/Qdv7JrFRqSgb4TFg==} + /discord-api-types@0.37.61: + resolution: {integrity: sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==} + dev: false - /discord.js@14.13.0: - resolution: {integrity: sha512-Kufdvg7fpyTEwANGy9x7i4od4yu5c6gVddGi5CKm4Y5a6sF0VBODObI3o0Bh7TGCj0LfNT8Qp8z04wnLFzgnbA==} + /discord-api-types@0.37.63: + resolution: {integrity: sha512-WbEDWj/1JGCIC1oCMIC4z9XbYY8PrWpV5eqFFQymJhJlHMqgIjqoYbU812X5oj5cwbRrEh6Va4LNLumB2Nt6IQ==} + dev: true + + /discord.js@14.14.1: + resolution: {integrity: sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==} engines: {node: '>=16.11.0'} dependencies: - '@discordjs/builders': 1.6.5 + '@discordjs/builders': 1.7.0 '@discordjs/collection': 1.5.3 - '@discordjs/formatters': 0.3.2 - '@discordjs/rest': 2.0.1 - '@discordjs/util': 1.0.1 - '@discordjs/ws': 1.0.1 + '@discordjs/formatters': 0.3.3 + '@discordjs/rest': 2.2.0 + '@discordjs/util': 1.0.2 + '@discordjs/ws': 1.0.2 '@sapphire/snowflake': 3.5.1 - '@types/ws': 8.5.8 - discord-api-types: 0.37.50 + '@types/ws': 8.5.9 + discord-api-types: 0.37.61 fast-deep-equal: 3.1.3 lodash.snakecase: 4.1.1 tslib: 2.6.2 - undici: 5.22.1 + undici: 5.27.2 ws: 8.14.2 transitivePeerDependencies: - bufferutil @@ -802,11 +1055,28 @@ packages: esutils: 2.0.3 dev: true + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} requiresBuild: true dev: false + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: false + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + /encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} requiresBuild: true @@ -839,6 +1109,10 @@ packages: engines: {node: '>=6'} dev: false + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -857,15 +1131,15 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint@8.52.0: - resolution: {integrity: sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==} + /eslint@8.53.0: + resolution: {integrity: sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) '@eslint-community/regexpp': 4.10.0 - '@eslint/eslintrc': 2.1.2 - '@eslint/js': 8.52.0 + '@eslint/eslintrc': 2.1.3 + '@eslint/js': 8.53.0 '@humanwhocodes/config-array': 0.11.13 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 @@ -942,11 +1216,64 @@ packages: engines: {node: '>=0.10.0'} dev: true + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /express-handlebars@7.1.2: + resolution: {integrity: sha512-ss9d3mBChOLTEtyfzXCsxlItUxpgS3i4cb/F70G6Q5ohQzmD12XB4x/Y9U6YboeeYBJZt7WQ5yUNu7ZSQ/EGyQ==} + engines: {node: '>=v16'} + dependencies: + glob: 10.3.10 + graceful-fs: 4.2.11 + handlebars: 4.7.8 + dev: false + + /express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - /fast-glob@3.3.1: - resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} dependencies: '@nodelib/fs.stat': 2.0.5 @@ -974,7 +1301,7 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} dependencies: - flat-cache: 3.1.1 + flat-cache: 3.2.0 dev: true /fill-range@7.0.1: @@ -984,6 +1311,21 @@ packages: to-regex-range: 5.0.1 dev: true + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + /find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -992,9 +1334,9 @@ packages: path-exists: 4.0.0 dev: true - /flat-cache@3.1.1: - resolution: {integrity: sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==} - engines: {node: '>=12.0.0'} + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} dependencies: flatted: 3.2.9 keyv: 4.5.4 @@ -1005,6 +1347,24 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: false + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + /fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -1051,6 +1411,15 @@ packages: dev: false optional: true + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: false + /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -1081,6 +1450,18 @@ packages: is-glob: 4.0.3 dev: true + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 5.0.0 + path-scurry: 1.10.1 + dev: false + /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} requiresBuild: true @@ -1105,12 +1486,18 @@ packages: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.3.1 + fast-glob: 3.3.2 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 dev: true + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.2 + dev: false + /got@11.8.6: resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} engines: {node: '>=10.19.0'} @@ -1118,7 +1505,7 @@ packages: '@sindresorhus/is': 4.6.0 '@szmarczak/http-timer': 4.0.6 '@types/cacheable-request': 6.0.3 - '@types/responselike': 1.0.2 + '@types/responselike': 1.0.3 cacheable-lookup: 5.0.4 cacheable-request: 7.0.4 decompress-response: 6.0.0 @@ -1132,16 +1519,44 @@ packages: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} requiresBuild: true dev: false - optional: true /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true + /handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.17.4 + dev: false + /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.2 + dev: false + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + /has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} dev: false @@ -1157,6 +1572,17 @@ packages: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} dev: false + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + /http-proxy-agent@4.0.1: resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} engines: {node: '>= 6'} @@ -1192,10 +1618,17 @@ packages: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} requiresBuild: true dependencies: - ms: 2.1.2 + ms: 2.1.3 dev: false optional: true + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + /iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -1261,6 +1694,11 @@ packages: dev: false optional: true + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + /is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: @@ -1305,6 +1743,15 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} requiresBuild: true + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: false + /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1411,6 +1858,11 @@ packages: engines: {node: '>=8'} dev: false + /lru-cache@10.0.3: + resolution: {integrity: sha512-B7gr+F6MkqB3uzINHXNctGieGsRTMwIBgxkp0yq/5BwcuDzD4A8wQpHQW6vDAm1uKSLQghmRdD9sKqf2vJ1cEg==} + engines: {node: 14 || >=16.14} + dev: false + /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -1456,11 +1908,25 @@ packages: dev: false optional: true + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + dev: false + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} dev: true + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + /micromatch@4.0.5: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} @@ -1469,6 +1935,24 @@ packages: picomatch: 2.3.1 dev: true + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + /mimic-response@1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} @@ -1484,6 +1968,17 @@ packages: dependencies: brace-expansion: 1.1.11 + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: false + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: false + /minipass-collect@1.0.2: resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} engines: {node: '>= 8'} @@ -1559,9 +2054,18 @@ packages: hasBin: true dev: false + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + requiresBuild: true + dev: false + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true @@ -1571,7 +2075,10 @@ packages: engines: {node: '>= 0.6'} requiresBuild: true dev: false - optional: true + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: false /node-addon-api@4.3.0: resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} @@ -1650,6 +2157,17 @@ packages: engines: {node: '>=0.10.0'} dev: false + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: false + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -1712,6 +2230,11 @@ packages: color-convert: 0.5.3 dev: false + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1725,12 +2248,23 @@ packages: /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - dev: true /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: false + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.0.3 + minipass: 5.0.0 + dev: false + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + dev: false + /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -1776,6 +2310,18 @@ packages: dev: false optional: true + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: false + /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: @@ -1783,10 +2329,20 @@ packages: once: 1.4.0 dev: false - /punycode@2.3.0: - resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - dev: true + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false + + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: false /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1797,6 +2353,11 @@ packages: engines: {node: '>=10'} dev: false + /random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + dev: false + /random-seed@0.3.0: resolution: {integrity: sha512-y13xtn3kcTlLub3HKWXxJNeC2qK4mB59evwZ5EkeRlolx+Bp2ztF7LbcZmyCnOqlHQrLnfuNbi1sVmm9lPDlDA==} engines: {node: '>= 0.6.0'} @@ -1804,6 +2365,21 @@ packages: json-stringify-safe: 5.0.1 dev: false + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + /readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -1820,6 +2396,10 @@ packages: resolve: 1.22.8 dev: false + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: false + /resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} dev: false @@ -1882,7 +2462,6 @@ packages: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} requiresBuild: true dev: false - optional: true /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} @@ -1896,26 +2475,84 @@ packages: dependencies: lru-cache: 6.0.0 + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + dev: false + /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: false + /set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: false + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} dependencies: shebang-regex: 3.0.0 - dev: true /shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - dev: true + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + object-inspect: 1.13.1 + dev: false /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: false + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: false + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -1951,6 +2588,11 @@ packages: dev: false optional: true + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: false + /sqlite3@5.1.6: resolution: {integrity: sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==} requiresBuild: true @@ -1978,9 +2620,9 @@ packages: dev: false optional: true - /streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} dev: false /string-width@4.2.3: @@ -1992,6 +2634,15 @@ packages: strip-ansi: 6.0.1 dev: false + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: false + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} requiresBuild: true @@ -2005,6 +2656,13 @@ packages: dependencies: ansi-regex: 5.0.1 + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: false + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -2054,6 +2712,21 @@ packages: is-number: 7.0.0 dev: true + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /tough-cookie@4.1.3: + resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: false + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: false @@ -2087,21 +2760,43 @@ packages: engines: {node: '>=10'} dev: true + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + /typescript@5.2.2: resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} engines: {node: '>=14.17'} hasBin: true dev: true - /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + /uglify-js@3.17.4: + resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + engines: {node: '>=0.8.0'} + hasBin: true + requiresBuild: true + dev: false + optional: true + + /uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + dependencies: + random-bytes: 1.0.0 dev: false - /undici@5.22.1: - resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==} + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + /undici@5.27.2: + resolution: {integrity: sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==} engines: {node: '>=14.0'} dependencies: - busboy: 1.6.0 + '@fastify/busboy': 2.1.0 dev: false /unique-filename@1.1.1: @@ -2120,17 +2815,44 @@ packages: dev: false optional: true + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: false + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: - punycode: 2.3.0 + punycode: 2.3.1 dev: true + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} requiresBuild: true dev: false + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: false @@ -2155,6 +2877,28 @@ packages: string-width: 4.2.3 dev: false + /wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + dev: false + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: false + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} requiresBuild: true diff --git a/src/commands/attack.ts b/src/commands/attack.ts new file mode 100644 index 0000000..4089443 --- /dev/null +++ b/src/commands/attack.ts @@ -0,0 +1,79 @@ +import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { getItem, getItemQuantity, formatItems, formatItem, weaponInventoryAutocomplete } from '../lib/rpg/items'; +import { Command } from '../types/index'; +import { initHealth, dealDamage, BLOOD_ITEM, BLOOD_ID, resetInvincible, INVINCIBLE_TIMER, getInvincibleMs } from '../lib/rpg/pvp'; +import { getBehavior, getBehaviors } from '../lib/rpg/behaviors'; +import { Right } from '../lib/util'; + +export default { + data: new SlashCommandBuilder() + .setName('attack') + .setDescription('Attack someone using a weapon you have') + .addStringOption(option => + option + .setName('weapon') + .setAutocomplete(true) + .setDescription('The weapon to use') + .setRequired(true) + ) + .addUserOption(option => + option + .setName('user') + .setRequired(true) + .setDescription('Who to attack with the weapon') + ) + .setDMPermission(false), + + execute: async (interaction: CommandInteraction) => { + if (!interaction.isChatInputCommand()) return; + + const member = interaction.member! as GuildMember; + await initHealth(member.id); + const weaponID = parseInt(interaction.options.getString('weapon', true)); + const target = interaction.options.getUser('user', true); + + await interaction.deferReply({ephemeral: true}); + + const weapon = await getItem(weaponID); + if (!weapon) return interaction.followUp('No such item exists!'); + if (weapon.type !== 'weapon') return interaction.followUp('That is not a weapon!'); + const inv = await getItemQuantity(member.id, weapon.id); + if (inv.quantity === 0) return interaction.followUp('You do not have this weapon!'); + const invinTimer = await getInvincibleMs(target.id); + if (invinTimer > 0) return interaction.followUp(`You can only attack this user (or if they perform an action first)!`); + + let dmg = weapon.maxStack; + const messages = []; + + const behaviors = await getBehaviors(weapon); + for (const itemBehavior of behaviors) { + const behavior = getBehavior(itemBehavior.behavior); + if (!behavior) continue; + if (!behavior.onAttack) continue; + const res = await behavior.onAttack({ + value: itemBehavior.value, + damage: dmg, + item: weapon, + user: member.id, + target: target.id, + }); + if (res instanceof Right) { + await interaction.followUp(`You tried to attack with ${formatItem(weapon)}... but failed!\n${res.getValue()}`); + return; + } else { + const { message, damage } = res.getValue(); + if (message) messages.push(message); + if (damage) dmg = damage; + } + } + + await dealDamage(target.id, dmg); + const newHealth = await getItemQuantity(target.id, BLOOD_ID); + + if (target.id !== member.id) await resetInvincible(member.id); + + await interaction.followUp(`You hit ${target} with ${formatItem(weapon)} for ${BLOOD_ITEM.emoji} **${dmg}** damage!\n${messages.map(m => `_${m}_\n`).join('')}They are now at ${formatItems(BLOOD_ITEM, newHealth.quantity)}.\nYou can attack them again (or if they perform an action first).`); + }, + + autocomplete: weaponInventoryAutocomplete, +} satisfies Command; \ No newline at end of file diff --git a/src/commands/change.ts b/src/commands/change.ts index 221f265..ea8f952 100644 --- a/src/commands/change.ts +++ b/src/commands/change.ts @@ -1,5 +1,6 @@ -import { CategoryChannel, GuildMember, EmbedBuilder, TextChannel, Interaction, ChannelType, SlashCommandBuilder } from 'discord.js'; +import { CategoryChannel, GuildMember, EmbedBuilder, TextChannel, CommandInteraction, ChannelType, SlashCommandBuilder } from 'discord.js'; import { knownServers } from '../lib/knownServers'; +import { Command } from '../types/index'; const rand = [ 'This change has no significance.', @@ -38,7 +39,7 @@ const nicknames = [ 'goobert' ]; -module.exports = { +export default { data: new SlashCommandBuilder() .setName('change') .setDescription('Change') @@ -51,9 +52,11 @@ module.exports = { serverWhitelist: [...knownServers.firepit, ...knownServers.fbi], - execute: async (interaction: Interaction, member: GuildMember) => { + execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; + const member = interaction.member! as GuildMember; + const what = interaction.options.getString('what', true); let title = `**${member.displayName}** changed the **${what}**`; let response; @@ -108,4 +111,4 @@ module.exports = { ephemeral: false, }); } -}; \ No newline at end of file +} satisfies Command; \ No newline at end of file diff --git a/src/commands/color.ts b/src/commands/color.ts index 56f4b24..318ecd8 100644 --- a/src/commands/color.ts +++ b/src/commands/color.ts @@ -1,8 +1,9 @@ -import { RoleCreateOptions, GuildMember, Interaction, EmbedBuilder, TextChannel, ActionRowBuilder, ButtonBuilder, ButtonStyle, SlashCommandBuilder } from 'discord.js'; +import { RoleCreateOptions, GuildMember, CommandInteraction, EmbedBuilder, TextChannel, ActionRowBuilder, ButtonBuilder, ButtonStyle, SlashCommandBuilder } from 'discord.js'; import { default as parseColor, Color } from 'parse-color'; import { isColorRole, COLOR_ROLE_SEPERATOR } from '../lib/assignableRoles'; import { knownServers } from '../lib/knownServers'; import * as log from '../lib/log'; +import { Command } from '../types/index'; const PREVIEW_DURATION = 1000 * 60; @@ -35,7 +36,7 @@ async function applyColor(member: GuildMember, color: Color) { await member.roles.add(role); } -module.exports = { +export default { data: new SlashCommandBuilder() .setName('color') .setDescription('Change your role color.') @@ -49,9 +50,11 @@ module.exports = { serverWhitelist: [...knownServers.firepit], - execute: async (interaction: Interaction, member: GuildMember) => { + execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; + const member = interaction.member! as GuildMember; + const color = interaction.options.getString('color'); const preview = interaction.options.getBoolean('preview'); @@ -129,4 +132,4 @@ module.exports = { }); } } -}; \ No newline at end of file +} satisfies Command; \ No newline at end of file diff --git a/src/commands/counter.ts b/src/commands/counter.ts index a4ed008..cb98c6f 100644 --- a/src/commands/counter.ts +++ b/src/commands/counter.ts @@ -1,7 +1,10 @@ -import { AutocompleteInteraction, Interaction, SlashCommandBuilder } from 'discord.js'; +import { AutocompleteInteraction, CommandInteraction, SlashCommandBuilder } from 'discord.js'; import { Counter, CounterUserLink, db } from '../lib/db'; -import { counterAutocomplete, counterConfigs, findCounter, getCounterConfigRaw, getOptions, parseConfig, setCounterConfig, toStringConfig, updateCounter } from '../lib/counter'; +import { counterAutocomplete, counterConfigs, findCounter, getCounterConfigRaw, getOptions, parseConfig, setCounterConfig, toStringConfig, updateCounter } from '../lib/rpg/counter'; import { outdent } from 'outdent'; +import { formatItem, formatItems, getItem, itemAutocomplete } from '../lib/rpg/items'; +import { Command } from '../types/index'; +import { set } from '../lib/autocomplete'; function extendOption(t: string) { return {name: t, value: t}; @@ -9,7 +12,7 @@ function extendOption(t: string) { const help = new Map([ ['message templates', outdent` - When using \`messageTemplate\`, \`messageTemplateIncrease\` or \`messageTemplateDecrease\`, you are providing a **template string**. + When using \`messageTemplate\`, \`messageTemplateIncrease\`, \`messageTemplateDecrease\`, \`messageTemplatePut\` or \`messageTemplateTake\`, you are providing a **template string**. A template string is a **specially-formatted** string with placeholder values. For instance, a template string like so: > **%user** has %action the counter by **%amt**. @@ -27,7 +30,7 @@ const help = new Map([ `] ]); -module.exports = { +export default { data: new SlashCommandBuilder() .setName('counter') .setDescription('[ADMIN] Counter management') @@ -143,7 +146,7 @@ module.exports = { .addStringOption(option => option .setName('key') - .setDescription('The codename. Best to leave descriptive for later; used in searching for counters') + .setDescription('Give your counter a simple name') .setRequired(true) ) .addStringOption(option => @@ -215,10 +218,29 @@ module.exports = { .setChoices(...[...help.keys()].map(extendOption)) ) ) + .addSubcommand(sub => + sub + .setName('link') + .setDescription('[ADMIN] THIS IS IRREVERSIBLE! Attach an item to this counter, letting you take or put items in.') + .addStringOption(opt => + opt + .setName('type') + .setDescription('The counter to operate on') + .setRequired(true) + .setAutocomplete(true) + ) + .addStringOption(opt => + opt + .setName('item') + .setDescription('The item') + .setAutocomplete(true) + .setRequired(true) + ) + ) .setDefaultMemberPermissions('0') .setDMPermission(false), - execute: async (interaction: Interaction) => { + execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; await interaction.deferReply({ephemeral: true}); @@ -233,10 +255,7 @@ module.exports = { try { counter = await findCounter(type, interaction.guildId!); } catch(err) { - await interaction.followUp({ - content: 'No such counter!' - }); - return; + return interaction.followUp('No such counter!'); } if (subcommand === 'add') { @@ -276,12 +295,7 @@ module.exports = { .where('producer', userType === 'producer') .first(); - if (!link) { - await interaction.followUp({ - content: `<@${user.id}> is not in the ${counter.emoji} **${userType}** allowlist!` - }); - return; - } + if (!link) return interaction.followUp(`<@${user.id}> is not in the ${counter.emoji} **${userType}** allowlist!`); await interaction.followUp({ content: `<@${user.id}> has been removed from the ${counter.emoji} **${userType}** allowlist.` @@ -350,15 +364,14 @@ module.exports = { try { counter = await findCounter(type, interaction.guildId!); } catch(err) { - await interaction.followUp({ - content: 'No such counter!' - }); - return; + return interaction.followUp('No such counter!'); } const config = await getCounterConfigRaw(counter); const key = interaction.options.getString('key', true); const value = interaction.options.getString('value', true); + + if (key === 'emoji' && counter.linkedItem) return interaction.followUp(`Cannot modify emoji - this counter is linked to ${formatItem(await getItem(counter.linkedItem))}`); const defaultConfig = counterConfigs.get(key); if (!defaultConfig) return interaction.followUp(`No config named \`${key}\` exists!`); @@ -366,7 +379,7 @@ module.exports = { const parsedValue = parseConfig(value, defaultConfig.type); const restringedValue = toStringConfig(parsedValue, defaultConfig.type); - await setCounterConfig(counter.id, key, restringedValue); + await setCounterConfig(counter, key, restringedValue); await interaction.followUp(`${counter.emoji} \`${key}\` is now \`${restringedValue}\`. (was \`${config.get(key) || toStringConfig(defaultConfig.default, defaultConfig.type)}\`)`); } else if (subcommand === 'delete') { @@ -376,10 +389,7 @@ module.exports = { try { counter = await findCounter(type, interaction.guildId!); } catch(err) { - await interaction.followUp({ - content: 'No such counter!' - }); - return; + return interaction.followUp('No such counter!'); } await db('counters') @@ -391,35 +401,64 @@ module.exports = { .delete(); await interaction.followUp({ - content: `The ${counter.emoji} counter has been removed. 😭` + content: `The ${counter.emoji} ${counter.key} counter has been removed. 😭` }); } else if (subcommand === 'list') { const counters = await db('counters') .where('guild', interaction.guildId!); - await interaction.followUp(counters.map(c => `${c.emoji} **${c.value}** <#${c.channel}>`).join('\n')); + await interaction.followUp(counters.map(c => `${c.emoji} ${c.key}: **${c.value}** <#${c.channel}>`).join('\n')); } else if (subcommand === 'help') { await interaction.followUp(help.get(interaction.options.getString('topic', true))!); + } else if (subcommand === 'link') { + const type = interaction.options.getString('type', true); + const itemID = parseInt(interaction.options.getString('item', true)); + + let counter; + try { + counter = await findCounter(type, interaction.guildId!); + } catch(err) { + return interaction.followUp('No such counter!'); + } + + const item = await getItem(itemID); + if (!item) return interaction.followUp('No such item exists!'); + if (item.untradable) return interaction.followUp('This item is untradable!'); + + await db('counters') + .where('id', counter.id) + .update({ + 'linkedItem': item.id, + 'emoji': item.emoji, + 'key': item.name, + 'value': 0 + }); + + await setCounterConfig(counter, 'canIncrement', 'false'); + await setCounterConfig(counter, 'canDecrement', 'false'); + await setCounterConfig(counter, 'min', '0'); + + await interaction.followUp(`Done. **The counter has been reset** to ${formatItems(item, 0)}. Users will not be able to take out or put in items until you enable this with \`canTake\` or \`canPut\`.\n\`canIncrement\` and \`canDecrement\` have also been **automatically disabled** and \`min\` has been set to **0**, and you are recommended to keep these values as such if you want to maintain balance in the universe.`); } } }, - autocomplete: async (interaction: AutocompleteInteraction) => {{ - const focused = interaction.options.getFocused(true); + autocomplete: set({ + type: counterAutocomplete, + item: itemAutocomplete, + value: async (interaction: AutocompleteInteraction) => { + const focused = interaction.options.getFocused(); - if (focused.name === 'type') { - return counterAutocomplete(interaction); - } else if (focused.name === 'value') { const type = interaction.options.getString('type', true); const counter = await findCounter(type, interaction.guildId!); const config = await getCounterConfigRaw(counter); const key = interaction.options.getString('key'); - if (!key) return interaction.respond([]); + if (!key) return []; const defaultConfig = counterConfigs.get(key); - if (!defaultConfig) return interaction.respond([]); + if (!defaultConfig) return []; const defaultOptions = getOptions(defaultConfig.type); @@ -432,20 +471,20 @@ module.exports = { value: `${toStringConfig(defaultConfig.default, defaultConfig.type)}`, name: `Default: ${toStringConfig(defaultConfig.default, defaultConfig.type)}` }, - ...defaultOptions.filter(s => s.startsWith(focused.value)).map(extendOption) + ...defaultOptions.filter(s => s.startsWith(focused)).map(extendOption) ]; - if (focused.value !== '' && !options.find(opt => opt.value === focused.value)) { + if (focused !== '' && !options.find(opt => opt.value === focused)) { options = [ { - value: focused.value, - name: focused.value + value: focused, + name: focused }, ...options ]; } - await interaction.respond(options); + return options; } - }} -}; \ No newline at end of file + }), +} satisfies Command; \ No newline at end of file diff --git a/src/commands/craft.ts b/src/commands/craft.ts new file mode 100644 index 0000000..53f2646 --- /dev/null +++ b/src/commands/craft.ts @@ -0,0 +1,146 @@ +import { AutocompleteInteraction, GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { CraftingStationCooldown, CustomCraftingRecipe, db } from '../lib/db'; +import { getStation, canUseStation, craftingStations, verb, CraftingStation } from '../lib/rpg/craftingStations'; +import { formatItem, getItemQuantity, formatItems, getMaxStack, giveItem, formatItemsArray } from '../lib/rpg/items'; +import { getRecipe, defaultRecipes, formatRecipe, resolveCustomRecipe } from '../lib/rpg/recipes'; +import { Command } from '../types/index'; +import { initHealth, resetInvincible } from '../lib/rpg/pvp'; +import { set } from '../lib/autocomplete'; + +export default { + data: new SlashCommandBuilder() + .setName('craft') + .setDescription('Craft an item with items you have') + .addStringOption(option => + option + .setName('station') + .setAutocomplete(true) + .setDescription('Which station to use') + .setRequired(true) + ) + .addStringOption(option => + option + .setName('recipe') + .setAutocomplete(true) + .setDescription('What to craft') + .setRequired(true) + ) + .setDMPermission(false), + + execute: async (interaction: CommandInteraction) => { + if (!interaction.isChatInputCommand()) return; + + const member = interaction.member! as GuildMember; + + await initHealth(member.id); + + const recipeID = parseInt(interaction.options.getString('recipe', true)); + + await interaction.deferReply({ephemeral: true}); + + const recipe = await getRecipe(recipeID); + if (!recipe) return interaction.followUp('Recipe does not exist!'); + + const station = getStation(recipe.station)!; + if (!canUseStation(member.id, station)) return interaction.followUp(`${station.emoji} You need ${formatItem(station.requires)} to use this station!`); + + for (const input of recipe.inputs) { + const inv = await getItemQuantity(member.id, input.item.id); + if (inv.quantity < input.quantity) return interaction.followUp(`You need ${formatItems(input.item, input.quantity)} for this recipe! (You have ${formatItems(input.item, inv.quantity)})`); + } + for (const req of recipe.requirements) { + const inv = await getItemQuantity(member.id, req.item.id); + if (inv.quantity < req.quantity) return interaction.followUp(`You need ${formatItems(req.item, req.quantity)} to begin this recipe! (You have ${formatItems(req.item, inv.quantity)}. Don't worry, these items will not be consumed.)`); + } + for (const out of recipe.outputs) { + const inv = await getItemQuantity(member.id, out.item.id); + if (inv.quantity + out.quantity > getMaxStack(out.item)) return interaction.followUp(`You do not have enough inventory storage for this recipe! (${formatItems(out.item, inv.quantity + out.quantity)} is bigger than the stack size of ${getMaxStack(out.item)}x.)`); + } + + let cooldown; + if (station.cooldown) { + cooldown = await db('craftingStationCooldowns') + .where('station', station.key) + .where('user', member.id) + .first(); + + if (cooldown && (cooldown.usedAt + station.cooldown * 1000) > Date.now()) + return interaction.followUp(`${station.emoji} You can use this station again !`); + } + + // proceed with crafting! + + for (const input of recipe.inputs) { + giveItem(member.id, input.item, -input.quantity); + } + const outputs = station.manipulateResults ? station.manipulateResults(recipe.outputs) : recipe.outputs; + for (const output of outputs) { + giveItem(member.id, output.item, output.quantity); + } + + let nextUsableAt; + if (station.cooldown) { + if (!cooldown) { + await db('craftingStationCooldowns') + .insert({ + station: station.key, + user: member.id, + usedAt: Date.now() + }); + } else { + await db('craftingStationCooldowns') + .where('station', station.key) + .where('user', member.id) + .update({ + usedAt: Date.now() + }); + } + + nextUsableAt = Date.now() + station.cooldown * 1000; + } + + await resetInvincible(member.id); + return interaction.followUp(`${station.emoji} ${verb(station)} ${formatItemsArray(outputs)}!${outputs.length === 1 ? `\n_${outputs[0].item.description}_` : ''}${nextUsableAt ? `\n${station.name} usable again ` : ''}`); + }, + + autocomplete: set({ + station: async (interaction: AutocompleteInteraction) => + (await Promise.all( + craftingStations + .map(async station => [station, await canUseStation(interaction.user.id, station)]) + )) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([_station, usable]) => usable) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .map(([station, _]) => station as CraftingStation) + .filter(station => station.name.toLowerCase().includes(interaction.options.getFocused().toLowerCase())) + .map(station => ({ + name: `${station.emoji} ${station.name}`, + value: station.key + })), + recipe: async (interaction: AutocompleteInteraction) => { + const focused = interaction.options.getFocused(); + const station = interaction.options.getString('station'); + + const foundDefaultRecipes = defaultRecipes + .filter(recipe => recipe.station === station) + .filter(recipe => recipe.outputs.filter(n => n.item.name.toLowerCase().includes(focused.toLowerCase())).length > 0); + + const customRecipes = await db('customCraftingRecipes') + .where('station', station); + + const resolvedCustomRecipes = await Promise.all(customRecipes.map(resolveCustomRecipe)); + + const foundCustomRecipes = resolvedCustomRecipes + .filter(recipe => recipe.outputs.filter(n => n.item.name.toLowerCase().includes(focused.toLowerCase())).length > 0); + + const recipes = [...foundDefaultRecipes, ...foundCustomRecipes]; + + return recipes + .map(recipe => ({ + name: formatRecipe(recipe, true), + value: recipe.id.toString() + })); + } + }), +} satisfies Command; \ No newline at end of file diff --git a/src/commands/decrease.ts b/src/commands/decrease.ts index 8b80a92..829f2aa 100644 --- a/src/commands/decrease.ts +++ b/src/commands/decrease.ts @@ -1,7 +1,8 @@ -import { GuildMember, Interaction, SlashCommandBuilder } from 'discord.js'; -import { changeCounterInteraction, counterAutocomplete } from '../lib/counter'; +import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { changeCounterInteraction, counterAutocomplete } from '../lib/rpg/counter'; +import { Command } from '../types/index'; -module.exports = { +export default { data: new SlashCommandBuilder() .setName('decrease') .setDescription('Decrease a counter') @@ -21,9 +22,11 @@ module.exports = { ) .setDMPermission(false), - execute: async (interaction: Interaction, member: GuildMember) => { + execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; + const member = interaction.member! as GuildMember; + const amount = Math.trunc(interaction.options.getInteger('amount') || 1); const type = interaction.options.getString('type')!; @@ -32,5 +35,5 @@ module.exports = { changeCounterInteraction(interaction, member, -amount, type); }, - autocomplete: counterAutocomplete -}; \ No newline at end of file + autocomplete: counterAutocomplete, +} satisfies Command; \ No newline at end of file diff --git a/src/commands/eat.ts b/src/commands/eat.ts new file mode 100644 index 0000000..5e389ee --- /dev/null +++ b/src/commands/eat.ts @@ -0,0 +1,64 @@ +import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { Command } from '../types/index'; +import { initHealth, resetInvincible } from '../lib/rpg/pvp'; +import { consumableInventoryAutocomplete, formatItem, formatItems, getItem, getItemQuantity, giveItem } from '../lib/rpg/items'; +import { getBehavior, getBehaviors } from '../lib/rpg/behaviors'; +import { Right } from '../lib/util'; + +export default { + data: new SlashCommandBuilder() + .setName('eat') + .setDescription('Eat an item from your inventory') + .addStringOption(option => + option + .setName('item') + .setAutocomplete(true) + .setDescription('The item to eat') + .setRequired(true) + ) + .setDMPermission(false), + + execute: async (interaction: CommandInteraction) => { + if (!interaction.isChatInputCommand()) return; + + const member = interaction.member! as GuildMember; + + await initHealth(member.id); + + await interaction.deferReply({ ephemeral: true }); + + const itemID = parseInt(interaction.options.getString('item', true)); + const item = await getItem(itemID); + if (!item) return await interaction.followUp('Item does not exist!'); + + const itemInv = await getItemQuantity(member.id, item.id); + if (itemInv.quantity <= 0) return await interaction.followUp(`You do not have ${formatItem(item)}!`); + + const behaviors = await getBehaviors(item); + + const messages = []; + for (const itemBehavior of behaviors) { + const behavior = getBehavior(itemBehavior.behavior); + if (!behavior) continue; + if (!behavior.onUse) continue; + const res = await behavior.onUse({ + value: itemBehavior.value, + item, + user: member.id + }); + if (res instanceof Right) { + await interaction.followUp(`You tried to eat ${formatItems(item, 1)}... but failed!\n${res.getValue()}`); + return; + } else { + messages.push(res.getValue()); + } + } + + await resetInvincible(member.id); + const newInv = await giveItem(member.id, item, -1); + + return await interaction.followUp(`You ate ${formatItems(item, 1)}!\n${messages.map(m => `_${m}_`).join('\n')}\nYou now have ${formatItems(item, newInv.quantity)}`); + }, + + autocomplete: consumableInventoryAutocomplete, +} satisfies Command; \ No newline at end of file diff --git a/src/commands/emotedump.ts b/src/commands/emotedump.ts index 8f3ab6d..11c4f8f 100644 --- a/src/commands/emotedump.ts +++ b/src/commands/emotedump.ts @@ -1,7 +1,8 @@ -import { GuildMember, EmbedBuilder, SlashCommandBuilder, Interaction } from 'discord.js'; +import { EmbedBuilder, SlashCommandBuilder, CommandInteraction } from 'discord.js'; import { writeTmpFile } from '../lib/util'; +import { Command } from '../types/index'; -module.exports = { +export default { data: new SlashCommandBuilder() .setName('emotedump') .setDescription('Dump every emote in the server for Gitea') @@ -14,11 +15,11 @@ module.exports = { .setMaxValue(512) ), - execute: async (interaction: Interaction, member: GuildMember) => { + execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; const size = interaction.options.getInteger('size') || 64; - const emojis = member.guild.emojis; + const emojis = interaction.guild!.emojis; const embed = new EmbedBuilder() .setDescription(`names: \`${emojis.cache.map(emote => emote.name).join(',')}\``); @@ -38,4 +39,4 @@ module.exports = { ephemeral: true }); } -}; \ No newline at end of file +} satisfies Command; \ No newline at end of file diff --git a/src/commands/garbagecollectroles.ts b/src/commands/garbagecollectroles.ts index 07ccf09..aa4018c 100644 --- a/src/commands/garbagecollectroles.ts +++ b/src/commands/garbagecollectroles.ts @@ -1,6 +1,7 @@ -import { Guild, GuildMember, Interaction, Role, SlashCommandBuilder } from 'discord.js'; +import { Guild, GuildMember, CommandInteraction, Role, SlashCommandBuilder } from 'discord.js'; import { isColorRole, isPronounRole } from '../lib/assignableRoles'; import { knownServers } from '../lib/knownServers'; +import { Command } from '../types/index'; async function fetchRoleMembers(role: Role) { const members = await role.guild.members.fetch(); @@ -21,7 +22,7 @@ async function garbageCollectRoles(guild: Guild, dryRun: boolean): Promise r) as Role[]; } -module.exports = { +export default { data: new SlashCommandBuilder() .setName('garbage-collect-roles') .setDescription('Garbage collect unused roles for colors and pronouns.') @@ -30,7 +31,7 @@ module.exports = { serverWhitelist: [...knownServers.firepit], - execute: async (interaction: Interaction, member: GuildMember) => { + execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; await interaction.deferReply({ @@ -38,7 +39,7 @@ module.exports = { }); const dryrun = interaction.options.getBoolean('dry-run'); - const colorRoles = await garbageCollectRoles(member.guild, dryrun || false); + const colorRoles = await garbageCollectRoles(interaction.guild!, dryrun || false); if (dryrun) { interaction.followUp({ @@ -52,4 +53,4 @@ module.exports = { }); } } -}; \ No newline at end of file +} satisfies Command; \ No newline at end of file diff --git a/src/commands/increase.ts b/src/commands/increase.ts index 54be345..c921e35 100644 --- a/src/commands/increase.ts +++ b/src/commands/increase.ts @@ -1,7 +1,8 @@ -import { GuildMember, Interaction, SlashCommandBuilder } from 'discord.js'; -import { changeCounterInteraction, counterAutocomplete } from '../lib/counter'; +import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { changeCounterInteraction, counterAutocomplete } from '../lib/rpg/counter'; +import { Command } from '../types/index'; -module.exports = { +export default { data: new SlashCommandBuilder() .setName('increase') .setDescription('Increase a counter') @@ -21,9 +22,11 @@ module.exports = { ) .setDMPermission(false), - execute: async (interaction: Interaction, member: GuildMember) => { + execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; + const member = interaction.member! as GuildMember; + const amount = Math.trunc(interaction.options.getInteger('amount') || 1); const type = interaction.options.getString('type')!; @@ -32,5 +35,5 @@ module.exports = { changeCounterInteraction(interaction, member, amount, type); }, - autocomplete: counterAutocomplete -}; \ No newline at end of file + autocomplete: counterAutocomplete, +} satisfies Command; \ No newline at end of file diff --git a/src/commands/inventory.ts b/src/commands/inventory.ts new file mode 100644 index 0000000..f50116e --- /dev/null +++ b/src/commands/inventory.ts @@ -0,0 +1,33 @@ +import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { ItemInventory, db } from '../lib/db'; +import { formatItems, getItem } from '../lib/rpg/items'; +import { initHealth } from '../lib/rpg/pvp'; +import { Command } from '../types/index'; + +export default { + data: new SlashCommandBuilder() + .setName('inventory') + .setDescription('Check your inventory') + .setDMPermission(false), + + execute: async (interaction: CommandInteraction) => { + if (!interaction.isChatInputCommand()) return; + + const member = interaction.member! as GuildMember; + + await initHealth(member.id); + + await interaction.deferReply({ephemeral: true}); + + const itemsList = await db('itemInventories') + .select('item', 'quantity') + .where('user', member.user.id); + + // kind of stupid kind of awful + const items = (await Promise.all(itemsList.map(async i => ({item: await getItem(i.item), quantity: i.quantity})))).filter(i => i.item && i.quantity !== 0); + + await interaction.followUp( + `Your inventory:\n${items.length === 0 ? '_Your inventory is empty!_' : items.map(i => `- ${formatItems(i.item!, i.quantity)}\n_${i.item!.description}_`).join('\n')}` + ); + } +} satisfies Command; \ No newline at end of file diff --git a/src/commands/investigate.ts b/src/commands/investigate.ts index 6315988..1de5be0 100644 --- a/src/commands/investigate.ts +++ b/src/commands/investigate.ts @@ -1,4 +1,5 @@ -import { GuildMember, EmbedBuilder, SlashCommandBuilder, Interaction } from 'discord.js'; +import { GuildMember, EmbedBuilder, SlashCommandBuilder, CommandInteraction } from 'discord.js'; +import { Command } from '../types/index'; const rand = require('random-seed').create(); const results = [ @@ -19,16 +20,18 @@ function seperate(l: string[]): string { return l.slice(0, -1).join(', ') + ' or ' + l.slice(-1); } -module.exports = { +export default { data: new SlashCommandBuilder() .setName('investigate') .setDescription('Investigate someone.') .addUserOption((option) => option.setName('who').setDescription('Investigate who?').setRequired(true)) .addBooleanOption((option) => option.setName('sheriff').setDescription('Switch to Sheriff-style investigation').setRequired(false)), - execute: async (interaction: Interaction, member: GuildMember) => { + execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; + const member = interaction.member! as GuildMember; + const who = interaction.options.getUser('who', true); const sheriff = interaction.options.getBoolean('sheriff'); let response; @@ -69,4 +72,4 @@ module.exports = { ephemeral: true, }); } -}; \ No newline at end of file +} satisfies Command; \ No newline at end of file diff --git a/src/commands/item.ts b/src/commands/item.ts new file mode 100644 index 0000000..9f97aac --- /dev/null +++ b/src/commands/item.ts @@ -0,0 +1,359 @@ +import { AutocompleteInteraction, CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { Counter, CustomCraftingRecipeItem, CustomItem, ItemBehavior, db } from '../lib/db'; +import { customItemAutocomplete, formatItem, formatItems, getCustomItem, getItem, giveItem, isDefaultItem, itemAutocomplete } from '../lib/rpg/items'; +import { Command } from '../types/index'; +import { formatRecipe, getCustomRecipe } from '../lib/rpg/recipes'; +import { behaviors, formatBehavior, getBehavior } from '../lib/rpg/behaviors'; +import { set } from '../lib/autocomplete'; + +//function extendOption(t: string) { +// return {name: t, value: t}; +//} + +export default { + data: new SlashCommandBuilder() + .setName('item') + .setDescription('[ADMIN] Create, edit and otherwise deal with custom items') + .addSubcommandGroup(grp => + grp + .setName('add') + .setDescription('[ADMIN] Create an item') + .addSubcommand(cmd => + cmd + .setName('plain') + .setDescription('A normal, functionless item') + .addStringOption(opt => + opt + .setName('name') + .setDescription('The item name') + .setRequired(true) + ) + .addStringOption(opt => + opt + .setName('emoji') + .setDescription('An emoji or symbol that could represent this item') + .setRequired(true) + ) + .addStringOption(opt => + opt + .setName('description') + .setDescription('A short description') + ) + .addIntegerOption(opt => + opt + .setName('maxstack') + .setDescription('Maximum amount of this item you\'re able to hold at once') + ) + .addBooleanOption(opt => + opt + .setName('untradable') + .setDescription('Can you give this item to other people?') + ) + ) + .addSubcommand(cmd => + cmd + .setName('weapon') + .setDescription('A weapon that you can attack things with') + .addStringOption(opt => + opt + .setName('name') + .setDescription('The item name') + .setRequired(true) + ) + .addStringOption(opt => + opt + .setName('emoji') + .setDescription('An emoji or symbol that could represent this item') + .setRequired(true) + ) + .addIntegerOption(opt => + opt + .setName('damage') + .setDescription('How much base damage this weapon is intended to deal') + .setRequired(true) + ) + .addStringOption(opt => + opt + .setName('description') + .setDescription('A short description') + ) + .addBooleanOption(opt => + opt + .setName('untradable') + .setDescription('Can you give this item to other people?') + ) + ) + .addSubcommand(cmd => + cmd + .setName('consumable') + .setDescription('Consumable item, usable once and never again') + .addStringOption(opt => + opt + .setName('name') + .setDescription('The item name') + .setRequired(true) + ) + .addStringOption(opt => + opt + .setName('emoji') + .setDescription('An emoji or symbol that could represent this item') + .setRequired(true) + ) + .addStringOption(opt => + opt + .setName('description') + .setDescription('A short description') + ) + .addIntegerOption(opt => + opt + .setName('maxstack') + .setDescription('Maximum amount of this item you\'re able to hold at once') + ) + .addBooleanOption(opt => + opt + .setName('untradable') + .setDescription('Can you give this item to other people?') + ) + ) + ) + .addSubcommand(cmd => + cmd + .setName('give') + .setDescription('[ADMIN] Give a user an item') + .addUserOption(opt => + opt + .setName('who') + .setDescription('The user') + .setRequired(true) + ) + .addStringOption(opt => + opt + .setName('item') + .setDescription('The item') + .setAutocomplete(true) + .setRequired(true) + ) + .addIntegerOption(opt => + opt + .setName('quantity') + .setDescription('Amount of items to give') + ) + ) + .addSubcommand(cmd => + cmd + .setName('delete') + .setDescription('[ADMIN] Delete a custom item') + .addStringOption(opt => + opt + .setName('customitem') + .setDescription('The item') + .setAutocomplete(true) + .setRequired(true) + ) + ) + .addSubcommandGroup(grp => + grp + .setName('behavior') + .setDescription('[ADMIN] Item behavior management') + .addSubcommand(cmd => + cmd + .setName('add') + .setDescription('[ADMIN] Give an item a behavior') + .addStringOption(opt => + opt + .setName('customitem') + .setDescription('The item') + .setAutocomplete(true) + .setRequired(true) + ) + .addStringOption(opt => + opt + .setName('behavior') + .setDescription('The behavior to add') + .setAutocomplete(true) + .setRequired(true) + ) + .addNumberOption(opt => + opt + .setName('value') + .setDescription('A value to assign the behavior, not always applicable') + ) + ) + .addSubcommand(cmd => + cmd + .setName('remove') + .setDescription('[ADMIN] Rid an item of a behavior') + .addStringOption(opt => + opt + .setName('customitem') + .setDescription('The item') + .setAutocomplete(true) + .setRequired(true) + ) + .addStringOption(opt => + opt + .setName('removebehavior') + .setDescription('The behavior to remove') + .setAutocomplete(true) + .setRequired(true) + ) + ) + ) + .setDefaultMemberPermissions('0') + .setDMPermission(false), + + execute: async (interaction: CommandInteraction) => { + if (!interaction.isChatInputCommand()) return; + + await interaction.deferReply({ephemeral: true}); + + const subcommand = interaction.options.getSubcommand(true); + const group = interaction.options.getSubcommandGroup(); + + if (group === 'add') { + const item = await db('customItems') + .insert({ + 'guild': interaction.guildId!, + 'name': interaction.options.getString('name', true).trim(), + 'description': interaction.options.getString('description') || undefined, + 'emoji': interaction.options.getString('emoji', true).trim(), + 'type': subcommand as 'plain' | 'weapon' | 'consumable', // kind of wild that ts makes you do this + 'maxStack': (interaction.options.getInteger('maxstack') || interaction.options.getInteger('damage')) || (subcommand === 'weapon' ? 1 : 64), + 'untradable': interaction.options.getBoolean('untradable') || false, + }) + .returning('*'); + + await interaction.followUp(`${formatItem(item[0])} has been successfully created! πŸŽ‰\nYou can now use \`/item behavior\` to give it some custom functionality.`); + } else if (group === 'behavior') { + const itemID = parseInt(interaction.options.getString('customitem', true)); + const item = await getCustomItem(itemID); + if (!item) return await interaction.followUp('No such item exists!'); + if (item.guild !== interaction.guildId) return await interaction.followUp('This item is from a different server! Nice try though'); + + if (subcommand === 'add') { + const behaviorName = interaction.options.getString('behavior', true); + const value = interaction.options.getNumber('value'); + + const behavior = getBehavior(behaviorName); + if (!behavior) return await interaction.followUp(`No such behavior ${behaviorName}!`); + + const existingBehavior = await db('itemBehaviors') + .where('item', item.id) + .where('behavior', behavior.name) + .first(); + + if (existingBehavior) { + return await interaction.followUp(`${formatItem(item)} already has **${formatBehavior(behavior, existingBehavior.value)}**!`); + } + + await db('itemBehaviors') + .insert({ + item: item.id, + behavior: behavior.name, + value: value || undefined, + }); + + return await interaction.followUp(`${formatItem(item)} now has **${formatBehavior(behavior, value || undefined)}**`); + } else if (subcommand === 'remove') { + const behaviorName = interaction.options.getString('removebehavior', true); + + const behavior = getBehavior(behaviorName); + if (!behavior) return await interaction.followUp(`No such behavior ${behaviorName}!`); + + const existingBehavior = await db('itemBehaviors') + .where('item', item.id) + .where('behavior', behavior.name) + .first(); + + if (!existingBehavior) { + return await interaction.followUp(`${formatItem(item)} does not have behavior \`${behaviorName}\`!`); + } + + await db('itemBehaviors') + .where('item', item.id) + .where('behavior', behavior.name) + .delete(); + + return await interaction.followUp(`Deleted behavior ${formatBehavior(behavior, existingBehavior.value)} from ${formatItem(item)}.`); + } + } else { + if (subcommand === 'give') { + const user = interaction.options.getUser('who', true); + const itemID = parseInt(interaction.options.getString('item', true)); + const quantity = interaction.options.getInteger('quantity') || 1; + + const item = await getItem(itemID); + if (!item) return interaction.followUp('No such item exists!'); + + if (!isDefaultItem(item)) { + if (item.guild !== interaction.guildId) return await interaction.followUp('This item is from a different server! Nice try though'); + } + + const inv = await giveItem(user.id, item, quantity); + + await interaction.followUp(`${user.toString()} now has ${formatItems(item, inv.quantity)}.`); + } else if (subcommand === 'delete') { + const itemID = parseInt(interaction.options.getString('customitem', true)); + + const item = await getItem(itemID); + if (!item) return interaction.followUp('No such item exists!'); + + const usedIn = await db('customCraftingRecipeItems') + .where('item', item.id); + + if (usedIn.length > 0) { + const recipes = (await Promise.all(usedIn.map(i => getCustomRecipe(i.id)))).filter(r => r !== undefined); + return interaction.followUp(`⚠️ This item is used in the following recipes:\n${recipes.map(r => `- ${formatRecipe(r!)}`).join('\n')}`); + } + + const linkedWith = await db('counters') + .where('linkedItem', item.id); + + if (linkedWith.length > 0) { + return interaction.followUp(`⚠️ This item is used in the following counters:\n${linkedWith.map(c => `- ${c.key} ${c.value} in <#${c.channel}>`).join('\n')}`); + } + + await db('customItems') + .where('id', item.id) + .delete(); + + interaction.followUp(`${formatItem(item)} has been deleted.`); + } + } + }, + + autocomplete: set({ + item: itemAutocomplete, + customitem: customItemAutocomplete, + behavior: async (interaction: AutocompleteInteraction) => { + const focused = interaction.options.getFocused(); + let foundBehaviors = behaviors.filter(b => b.name.toLowerCase().includes(focused.toLowerCase())); + const itemID = interaction.options.getString('customitem'); + if (itemID) { + const item = await getItem(parseInt(itemID)); + if (item) { + foundBehaviors = foundBehaviors.filter(b => b.type === item.type); + } + } + + return foundBehaviors.map(b => ({name: `${b.type}:${b.name} - ${b.description}`, value: b.name})); + }, + removebehavior: async (interaction: AutocompleteInteraction) => { + const focused = interaction.options.getFocused(); + const itemID = interaction.options.getString('customitem'); + if (!itemID) return []; + const behaviors = await db('itemBehaviors') + .where('item', itemID); + + const foundBehaviors = behaviors + .map(b => ({ behavior: getBehavior(b.behavior)!, value: b.value })) + .filter(b => b.behavior) + .filter(b => b.behavior.name.toLowerCase().includes(focused.toLowerCase())); + + return foundBehaviors.map(b => ({ + name: `${b.behavior.type}:${formatBehavior(b.behavior, b.value)} - ${b.behavior.description}`, + value: b.behavior.name + })); + }, + }), +} satisfies Command; \ No newline at end of file diff --git a/src/commands/markov.ts b/src/commands/markov.ts index 37de9fa..67efc4a 100644 --- a/src/commands/markov.ts +++ b/src/commands/markov.ts @@ -1,5 +1,6 @@ -import { GuildMember, SlashCommandBuilder, Interaction, messageLink } from 'discord.js'; +import { GuildMember, SlashCommandBuilder, CommandInteraction, messageLink } from 'discord.js'; import { getTextResponsePrettyPlease, randomWord, sendSegments, startGame } from '../lib/game'; +import { Command } from '../types/index'; const END_TEMPLATES = [ 'Alright! Here\'s the messages you all conjured:', @@ -8,7 +9,7 @@ const END_TEMPLATES = [ 'That does it! Here\'s what you\'ve all cooked up together:' ]; -module.exports = { +export default { data: new SlashCommandBuilder() .setName('markov') .setDescription('Play a Markov chain game') @@ -28,9 +29,11 @@ module.exports = { .setMinValue(1) .setMaxValue(100)), - execute: async (interaction: Interaction, member: GuildMember) => { + execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; + const member = interaction.member! as GuildMember; + const context = interaction.options.getInteger('context') || 3; const maxIterations = interaction.options.getInteger('iterations') || 10; @@ -72,4 +75,4 @@ module.exports = { }); }); } -}; \ No newline at end of file +} satisfies Command; \ No newline at end of file diff --git a/src/commands/monitor.ts b/src/commands/monitor.ts index 559c248..e9d990b 100644 --- a/src/commands/monitor.ts +++ b/src/commands/monitor.ts @@ -1,12 +1,14 @@ -import { EmbedBuilder, Interaction, SlashCommandBuilder } from 'discord.js'; +import { EmbedBuilder, CommandInteraction, SlashCommandBuilder } from 'discord.js'; import got from 'got'; import { knownServers } from '../lib/knownServers'; +import { Command } from '../types/index'; + const rand = require('random-seed').create(); const imagesEndpoint = 'https://commons.wikimedia.org/w/api.php?action=query&cmlimit=500&cmtitle=Category%3ALiminal_spaces&cmtype=file&list=categorymembers&format=json'; const imageEndpoint = 'https://commons.wikimedia.org/w/api.php?action=query&piprop=thumbnail&pithumbsize=200&prop=pageimages&titles={}&format=json'; -module.exports = { +export default { data: new SlashCommandBuilder() .setName('monitor') .setDescription('Monitor') @@ -19,7 +21,7 @@ module.exports = { serverWhitelist: [...knownServers.firepit_extended, ...knownServers.fbi], - execute: async (interaction: Interaction) => { + execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; await interaction.deferReply({ephemeral: false}); @@ -49,4 +51,4 @@ module.exports = { embeds: [embed] }); } -}; \ No newline at end of file +} satisfies Command; \ No newline at end of file diff --git a/src/commands/pronouns.ts b/src/commands/pronouns.ts index c927211..bee3d11 100644 --- a/src/commands/pronouns.ts +++ b/src/commands/pronouns.ts @@ -1,13 +1,14 @@ -import { RoleCreateOptions, GuildMember, Interaction, SlashCommandBuilder } from 'discord.js'; +import { RoleCreateOptions, GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; import { pronouns, PRONOUN_ROLE_SEPERATOR } from '../lib/assignableRoles'; import { knownServers } from '../lib/knownServers'; import * as log from '../lib/log'; +import { Command } from '../types/index'; function extendOption(t: string) { return {name: t, value: t}; } -module.exports = { +export default { data: new SlashCommandBuilder() .setName('pronoun') .setDescription('Give yourself a pronoun or two.') @@ -21,9 +22,11 @@ module.exports = { serverWhitelist: [...knownServers.firepit], - execute: async (interaction: Interaction, member: GuildMember) => { + execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; + const member = interaction.member! as GuildMember; + await interaction.deferReply({ ephemeral: true }); @@ -57,4 +60,4 @@ module.exports = { }); } } -}; \ No newline at end of file +} satisfies Command; \ No newline at end of file diff --git a/src/commands/put.ts b/src/commands/put.ts new file mode 100644 index 0000000..a81140a --- /dev/null +++ b/src/commands/put.ts @@ -0,0 +1,42 @@ +import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { changeLinkedCounterInteraction, linkedCounterAutocomplete } from '../lib/rpg/counter'; +import { Command } from '../types/index'; +import { initHealth } from '../lib/rpg/pvp'; + +export default { + data: new SlashCommandBuilder() + .setName('put') + .setDescription('Put an item from your inventory into the counter') + .addStringOption(option => + option + .setName('type') + .setAutocomplete(true) + .setDescription('The name of the counter') + .setRequired(true) + ) + .addIntegerOption((option) => + option + .setName('amount') + .setRequired(false) + .setDescription('Amount of items to put in') + .setMinValue(1) + ) + .setDMPermission(false), + + execute: async (interaction: CommandInteraction) => { + if (!interaction.isChatInputCommand()) return; + + const member = interaction.member! as GuildMember; + + await initHealth(member.id); + + const amount = Math.trunc(interaction.options.getInteger('amount') || 1); + const type = interaction.options.getString('type')!; + + await interaction.deferReply({ephemeral: true}); + + changeLinkedCounterInteraction(interaction, member, amount, type); + }, + + autocomplete: linkedCounterAutocomplete, +} satisfies Command; \ No newline at end of file diff --git a/src/commands/recipe.ts b/src/commands/recipe.ts new file mode 100644 index 0000000..98e6428 --- /dev/null +++ b/src/commands/recipe.ts @@ -0,0 +1,254 @@ +import { ActionRowBuilder, AutocompleteInteraction, ButtonBuilder, ButtonStyle, CommandInteraction, ComponentType, Events, ModalBuilder, SlashCommandBuilder, StringSelectMenuBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; +import { Command } from '../types/index'; +import { Items, getItem } from '../lib/rpg/items'; +import { formatRecipe, getCustomRecipe, resolveCustomRecipe } from '../lib/rpg/recipes'; +import { craftingStations, getStation } from '../lib/rpg/craftingStations'; +import { CustomCraftingRecipe, CustomCraftingRecipeItem, db } from '../lib/db'; + +export default { + data: new SlashCommandBuilder() + .setName('recipe') + .setDescription('[ADMIN] Manage custom recipes for items') + .addSubcommand(sub => + sub + .setName('create') + .setDescription('[ADMIN] Create a custom recipe') + ) + .addSubcommand(sub => + sub + .setName('delete') + .setDescription('[ADMIN] Delete a custom recipe') + .addStringOption(opt => + opt + .setName('recipe') + .setAutocomplete(true) + .setDescription('Which recipe to remove') + .setRequired(true) + ) + ) + .setDMPermission(false) + .setDefaultMemberPermissions(0), + + execute: async (interaction: CommandInteraction) => { + if (!interaction.isChatInputCommand()) return; + + await interaction.deferReply({ ephemeral: true }); + + const sub = interaction.options.getSubcommand(true); + + if (sub === 'create') { + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId(`recipe-create-${interaction.guildId}`).setLabel('I\'ve got my string ready!').setStyle(ButtonStyle.Primary) + ); + await interaction.followUp({ + ephemeral: true, + content: `To create a recipe, go here: ${interaction.client.config.siteURL}/create-recipe/?guild=${interaction.guildId}\nOnce done, click the button below and paste the resulting string in.`, + components: [row] + }); + } else if (sub === 'delete') { + const recipeID = interaction.options.getString('recipe', true); + const recipe = await getCustomRecipe(parseInt(recipeID)); + + if (!recipe) return interaction.followUp('Recipe does no exist!'); + + await db('customCraftingRecipes') + .where('id', recipe.id) + .delete(); + + await db('customCraftingRecipeItems') + .where('id', recipe.id) + .delete(); + + await interaction.followUp(`Deleted recipe ${formatRecipe(recipe)}`); + } + }, + + async onClientReady(bot) { + bot.on(Events.InteractionCreate, async (interaction) => { + if (!('customId' in interaction)) return; + const id = interaction.customId; + if (!id.startsWith('recipe-')) return; + if (!interaction.member) return; + + if (id.startsWith('recipe-create-')) { + const guildID = id.split('-')[2]; + + if (interaction.isMessageComponent()) { + const modal = new ModalBuilder() + .setCustomId(interaction.customId) + .setTitle('Recipe Creator'); + + const input = new TextInputBuilder() + .setCustomId('recipe-create-textbox') + .setLabel('Paste in your recipe string here:') + .setStyle(TextInputStyle.Paragraph) + .setRequired(true); + + const row = new ActionRowBuilder().addComponents(input); + modal.addComponents(row); + interaction.showModal(modal); + } else if (interaction.isModalSubmit()) { + const field = interaction.fields.getField('recipe-create-textbox', ComponentType.TextInput); + const recipeString = field.value.trim(); + + await interaction.deferReply({ ephemeral: true }); + + let parsed; + try { + parsed = await Promise.all( + recipeString + .split('|') + .map(async items => + items === '' ? + [] : + await Promise.all( + items + .split(';') + .map(itemStack => + itemStack.split(',') + ) + .map(async ([itemID, quantity]) => ( + { + item: (await getItem(parseInt(itemID)))!, + quantity: parseInt(quantity) + } + )) + ) + ) + ) as Items[][]; + } catch (err) { + await interaction.followUp(`This is not a valid string!: \`${(err as Error).message}\``); + return; + } + + const + inputs = parsed[0] || [], + requirements = parsed[1] || [], + outputs = parsed[2] || []; + + const recipe = { + inputs, requirements, outputs, + station: 'hands', + id: 0 + }; + + const components = [ + new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() + .addOptions( + ...craftingStations + .map(station => ({ + label: `${station.emoji} ${station.name}`, + value: station.key, + description: station.description + })) + ) + .setMinValues(1) + .setMaxValues(1) + .setCustomId('recipe-select-station') + ), + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('recipe-select-done') + .setLabel('Done') + .setStyle(ButtonStyle.Primary) + .setDisabled(true) + ) + ]; + + const msg = await interaction.followUp({ + content: `${formatRecipe(recipe)}\n_Select a crafting station, and you're good to go!_`, + components: components, + fetchReply: true + }); + + const selectCollector = msg.createMessageComponentCollector({ + componentType: ComponentType.StringSelect, + time: 60_000 * 5, + }); + + selectCollector.on('collect', selectInteraction => { + const newStation = selectInteraction.values[0]; + recipe.station = newStation; + components[1].components[0].setDisabled(false); + interaction.editReply({ + content: `${formatRecipe(recipe)}\n_Select a crafting station, and you're good to go!_`, + components: components + }); + const station = getStation(newStation); + selectInteraction.reply({ + content: `Set station to ${station?.emoji} **${station?.name}**`, + ephemeral: true + }); + }); + selectCollector.on('end', () => { + interaction.editReply({ + content: msg.content, + components: [] + }); + }); + + const buttonInteraction = await msg.awaitMessageComponent({ componentType: ComponentType.Button, time: 60_000 * 5 }); + selectCollector.stop(); + + const [customRecipe] = await db('customCraftingRecipes') + .insert({ + guild: guildID, + station: recipe.station + }) + .returning('id'); + + for (const input of recipe.inputs) { + await db('customCraftingRecipeItems') + .insert({ + id: customRecipe.id, + item: input.item.id, + quantity: input.quantity, + type: 'input' + }); + } + for (const req of recipe.requirements) { + await db('customCraftingRecipeItems') + .insert({ + id: customRecipe.id, + item: req.item.id, + quantity: req.quantity, + type: 'requirement' + }); + } + for (const output of recipe.outputs) { + await db('customCraftingRecipeItems') + .insert({ + id: customRecipe.id, + item: output.item.id, + quantity: output.quantity, + type: 'output' + }); + } + + buttonInteraction.reply({ + ephemeral: true, + content: 'Your recipe has been created πŸŽ‰' + }); + } + } + }); + }, + + autocomplete: async (interaction: AutocompleteInteraction) => { + const focused = interaction.options.getFocused(); + + const customRecipes = await db('customCraftingRecipes'); + + const resolvedCustomRecipes = await Promise.all(customRecipes.map(resolveCustomRecipe)); + + const foundCustomRecipes = resolvedCustomRecipes + .filter(recipe => recipe.outputs.filter(n => n.item.name.toLowerCase().includes(focused.toLowerCase())).length > 0); + + return foundCustomRecipes + .map(recipe => ({ + name: formatRecipe(recipe, true), + value: recipe.id.toString() + })); + }, +} satisfies Command; \ No newline at end of file diff --git a/src/commands/subscribe.ts b/src/commands/subscribe.ts index 9749945..e84e45b 100644 --- a/src/commands/subscribe.ts +++ b/src/commands/subscribe.ts @@ -1,7 +1,8 @@ -import { Interaction, SlashCommandBuilder } from 'discord.js'; +import { CommandInteraction, SlashCommandBuilder } from 'discord.js'; import { isSubscribed, subscribe, timeAnnouncements, unsubscribe } from '../lib/subscriptions'; +import { Command } from '../types/index'; -module.exports = { +export default { data: new SlashCommandBuilder() .setName('subscribe') .setDescription('[ADMIN] Subscribe/unsubscribe to a time announcement') @@ -14,7 +15,7 @@ module.exports = { ) .setDefaultMemberPermissions('0'), - execute: async (interaction: Interaction) => { + execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; await interaction.deferReply({ephemeral: true}); @@ -34,4 +35,4 @@ module.exports = { }); } } -}; \ No newline at end of file +} satisfies Command; \ No newline at end of file diff --git a/src/commands/survey.ts b/src/commands/survey.ts index 42d40b9..47912e8 100644 --- a/src/commands/survey.ts +++ b/src/commands/survey.ts @@ -1,6 +1,7 @@ import { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, Client, Collection, MessageComponentInteraction, StringSelectMenuBuilder, ModalBuilder, TextChannel, TextInputStyle, Message, ButtonStyle, ComponentType, APIButtonComponentWithCustomId, Events, TextInputBuilder, SlashCommandBuilder } from 'discord.js'; import * as fs from 'fs/promises'; import { knownServers } from '../lib/knownServers'; +import { Command } from '../types/index'; const RESPONSES_CHANNEL = '983762973858361364'; const GENERAL_CHANNEL = '587108210683412493'; @@ -599,7 +600,7 @@ async function advanceSurvey(userId: string, dontAdvanceProgress = false) { } } -module.exports = { +export default { data: new SlashCommandBuilder() .setName('createsurvey') .setDescription('Re-create the survey button'), @@ -621,7 +622,7 @@ module.exports = { }); }, - onClientReady: (bot: Client) => { + onClientReady: async (bot: Client) => { bot.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isMessageComponent()) return; if (interaction.isModalSubmit()) return; @@ -732,6 +733,7 @@ module.exports = { }); bot.on(Events.InteractionCreate, interaction => { if (!interaction.isModalSubmit()) return; + if (!interaction.customId.startsWith('survey-')) return; if (!interaction.member) return; const member = interaction.member as GuildMember; @@ -758,4 +760,4 @@ module.exports = { advanceSurvey(member.id); }); } -}; +} satisfies Command; diff --git a/src/commands/take.ts b/src/commands/take.ts new file mode 100644 index 0000000..5ad607f --- /dev/null +++ b/src/commands/take.ts @@ -0,0 +1,42 @@ +import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { changeLinkedCounterInteraction, linkedCounterAutocomplete } from '../lib/rpg/counter'; +import { initHealth } from '../lib/rpg/pvp'; +import { Command } from '../types/index'; + +export default { + data: new SlashCommandBuilder() + .setName('take') + .setDescription('Take an item from a counter') + .addStringOption(option => + option + .setName('type') + .setAutocomplete(true) + .setDescription('The name of the counter') + .setRequired(true) + ) + .addIntegerOption((option) => + option + .setName('amount') + .setRequired(false) + .setDescription('Amount of items to take') + .setMinValue(1) + ) + .setDMPermission(false), + + execute: async (interaction: CommandInteraction) => { + if (!interaction.isChatInputCommand()) return; + + const member = interaction.member! as GuildMember; + + await initHealth(member.id); + + const amount = Math.trunc(interaction.options.getInteger('amount') || 1); + const type = interaction.options.getString('type')!; + + await interaction.deferReply({ephemeral: true}); + + changeLinkedCounterInteraction(interaction, member, -amount, type); + }, + + autocomplete: linkedCounterAutocomplete, +} satisfies Command; \ No newline at end of file diff --git a/src/commands/twosentencehorror.ts b/src/commands/twosentencehorror.ts index 6c1f54d..42bc2db 100644 --- a/src/commands/twosentencehorror.ts +++ b/src/commands/twosentencehorror.ts @@ -1,6 +1,7 @@ -import { GuildMember, SlashCommandBuilder, Interaction, messageLink, User } from 'discord.js'; +import { GuildMember, SlashCommandBuilder, CommandInteraction, messageLink, User } from 'discord.js'; import { getTextResponsePrettyPlease, sendSegments, startGame } from '../lib/game'; import { shuffle } from 'd3-array'; +import { Command } from '../types/index'; const horrorStarters = [ 'I was playing with my boobs.', @@ -59,13 +60,15 @@ function shift(arr: T[]): T[] { return [...arr.slice(1), arr[0]]; } -module.exports = { +export default { data: new SlashCommandBuilder() .setName('twosentencehorror') .setDescription('Communally create the worst horror stories known to man'), - execute: async (interaction: Interaction, member: GuildMember) => { + execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; + + const member = interaction.member! as GuildMember; startGame(interaction, member.user, 'Two Sentence Horror', async (players, channel) => { players = shuffle(players); @@ -106,4 +109,4 @@ module.exports = { }); }); } -}; \ No newline at end of file +} satisfies Command; \ No newline at end of file diff --git a/src/commands/use.ts b/src/commands/use.ts new file mode 100644 index 0000000..25dc2d1 --- /dev/null +++ b/src/commands/use.ts @@ -0,0 +1,62 @@ +import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { Command } from '../types/index'; +import { initHealth, resetInvincible } from '../lib/rpg/pvp'; +import { formatItem, getItem, getItemQuantity, plainInventoryAutocomplete } from '../lib/rpg/items'; +import { getBehavior, getBehaviors } from '../lib/rpg/behaviors'; +import { Right } from '../lib/util'; + +export default { + data: new SlashCommandBuilder() + .setName('use') + .setDescription('Use an item from your inventory') + .addStringOption(option => + option + .setName('item') + .setAutocomplete(true) + .setDescription('The item to use') + .setRequired(true) + ) + .setDMPermission(false), + + execute: async (interaction: CommandInteraction) => { + if (!interaction.isChatInputCommand()) return; + + const member = interaction.member! as GuildMember; + + await initHealth(member.id); + + await interaction.deferReply({ ephemeral: true }); + + const itemID = parseInt(interaction.options.getString('item', true)); + const item = await getItem(itemID); + if (!item) return await interaction.followUp('Item does not exist!'); + + const itemInv = await getItemQuantity(member.id, item.id); + if (itemInv.quantity <= 0) return await interaction.followUp(`You do not have ${formatItem(item)}!`); + + const behaviors = await getBehaviors(item); + + const messages = []; + for (const itemBehavior of behaviors) { + const behavior = getBehavior(itemBehavior.behavior); + if (!behavior) continue; + if (!behavior.onUse) continue; + const res = await behavior.onUse({ + value: itemBehavior.value, + item, + user: member.id + }); + if (res instanceof Right) { + await interaction.followUp(`You tried to use ${formatItem(item)}... but failed!\n${res.getValue()}`); + return; + } else { + messages.push(res.getValue()); + } + } + + await resetInvincible(member.id); + return await interaction.followUp(`You used ${formatItem(item)}!\n${messages.map(m => `_${m}_`).join('\n')}`); + }, + + autocomplete: plainInventoryAutocomplete, +} satisfies Command; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 3f7b982..b88fbda 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,15 @@ import { Client, GatewayIntentBits, Events, Collection, CommandInteraction, CommandInteractionOption, ApplicationCommandOptionType } from 'discord.js'; import * as fs from 'fs'; -const { token } = JSON.parse(fs.readFileSync('./config.json', 'utf8')); +const { token, sitePort, siteURL, clientId, clientSecret } = JSON.parse(fs.readFileSync('./config.json', 'utf8')); import * as path from 'path'; import { initializeAnnouncements } from './lib/subscriptions'; import * as log from './lib/log'; import chalk from 'chalk'; import prettyBytes from 'pretty-bytes'; +import { Command } from './types/index'; +import { startServer } from './web/web'; +import { init as initPVP } from './lib/rpg/pvp'; +import { autocomplete } from './lib/autocomplete'; const bot = new Client({ intents: [ @@ -19,9 +23,16 @@ const bot = new Client({ ], }); +bot.config = { + token, sitePort, siteURL, clientId, clientSecret +}; + async function init() { log.nonsense('booting chip...'); + log.nonsense('starting up web interface...'); + await startServer(bot, sitePort); + log.nonsense('setting up connection...'); try { @@ -31,6 +42,8 @@ async function init() { log.error(`${chalk.bold('emergency mode could not be established.')} shutting down.`); process.exit(1); } + + initPVP(bot); } bot.on(Events.ClientReady, async () => { @@ -43,7 +56,7 @@ bot.on(Events.ClientReady, async () => { bot.commands = new Collection(); const cmdFiles = fs.readdirSync(path.join(__dirname, './commands')).filter((file) => file.endsWith('.js')); for (const file of cmdFiles) { - const cmd = (await import(`./commands/${file}`)); + const cmd = (await import(`./commands/${file}`)).default as Command; bot.commands.set(cmd.data.name, cmd); if (cmd.onClientReady) cmd.onClientReady(bot); } @@ -90,7 +103,7 @@ bot.on(Events.InteractionCreate, async (interaction) => { log.nonsense(stringifyCommand(interaction)); try { - await command.execute(interaction, interaction.member); + await command.execute(interaction); } catch (error) { if (interaction.isRepliable() && !interaction.replied && !interaction.deferred) interaction.reply({ content: `\`ERROR\`\n\`\`\`\n${error}\n\`\`\``, ephemeral: true }); if (interaction.deferred) interaction.followUp(`\`ERROR\`\n\`\`\`\n${error}\n\`\`\``); @@ -101,7 +114,8 @@ bot.on(Events.InteractionCreate, async (interaction) => { if (!command) return; try { - await command.autocomplete(interaction); + if (!command.autocomplete) throw `Trying to invoke autocomplete for command ${interaction.commandName} which does not have it defined`; + await autocomplete(command.autocomplete)(interaction); } catch (error) { log.error(error); } diff --git a/src/lib/autocomplete.ts b/src/lib/autocomplete.ts new file mode 100644 index 0000000..23af2c3 --- /dev/null +++ b/src/lib/autocomplete.ts @@ -0,0 +1,27 @@ +import { AutocompleteInteraction, ApplicationCommandOptionChoiceData } from 'discord.js'; +import * as log from './log'; + +export type Autocomplete = (interaction: AutocompleteInteraction) => Promise[]> + +export function set(fns: Record): Autocomplete { + return async (interaction: AutocompleteInteraction) => { + const focused = interaction.options.getFocused(true); + const fn = fns[focused.name]; + + if (!fn) return []; + + return fn(interaction); + }; +} +export function autocomplete(fn: Autocomplete): (interaction: AutocompleteInteraction) => Promise { + return async (interaction: AutocompleteInteraction) => { + try { + const arr = await fn(interaction); + if (arr.length > 25) log.warn(`Autocomplete for ${interaction.options.getFocused(true).name} exceeded limit of 25 autocomplete results`); + return interaction.respond(arr.slice(0, 25)); + } catch (err) { + log.error(err); + return interaction.respond([]); + } + }; +} \ No newline at end of file diff --git a/src/lib/counter.ts b/src/lib/counter.ts deleted file mode 100644 index 5c04b83..0000000 --- a/src/lib/counter.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { Client, CommandInteraction, GuildMember, EmbedBuilder, TextChannel, AutocompleteInteraction } from 'discord.js'; -import { getSign } from './util'; -import { Counter, CounterConfiguration, CounterUserLink, db } from './db'; - -export async function getCounter(id: number) { - const counter = await db('counters') - .select('value') - .where('id', id) - .first(); - - if (!counter) throw 'No such counter'; - - return counter.value; -} - -export async function changeCounter(id: number, delta: number) { - const value = await getCounter(id); - const newValue = value + delta; - - await db('counters') - .where('id', id) - .update({ - 'value': newValue - }); - - return newValue; -} - -export async function getCounterData(id: number) { - const counter = await db('counters') - .select('*') - .where('id', id) - .first(); - - if (!counter) throw 'No such counter'; - - return counter; -} - -export async function findCounter(key: string, guild: string) { - const counter = await db('counters') - .select('*') - .where('key', key) - .where('guild', guild) - .first(); - - if (!counter) throw 'No such counter'; - - return counter; -} - -export async function getCounterConfigRaw(counter: Counter) { - const configs = await db('counterConfigurations') - .select('configName', 'value') - .where('id', counter.id); - - const config = new Map(); - configs.forEach(({ configName, value }) => { - config.set(configName, value); - }); - - // just the ugly way of life - config.set('emoji', counter.emoji); - - return config; -} - -export async function getCounterConfig(id: number, key: string) { - const config = await db('counterConfigurations') - .select('value') - .where('id', id) - .where('configName', key) - .first(); - - const valueStr = config?.value; - let value; - if (valueStr) { - value = parseConfig(valueStr, counterConfigs.get(key)!.type); - } else { - value = counterConfigs.get(key)!.default; - } - - return value; -} - -export async function setCounterConfig(id: number, option: string, value: string) { - // just the ugly way of life - if (option === 'emoji') { - await db('counters') - .where('id', id) - .update({ - 'emoji': value - }); - return; - } - - const updated = await db('counterConfigurations') - .update({ - value: value - }) - .where('id', id) - .where('configName', option); - - if (updated === 0) { - await db('counterConfigurations') - .insert({ - 'id': id, - 'configName': option, - 'value': value - }); - } -} - -export enum ConfigType { - Bool, - String, - Number -} - -export function parseConfig(str: string, type: ConfigType.Bool): boolean -export function parseConfig(str: string, type: ConfigType.String): string -export function parseConfig(str: string, type: ConfigType.Number): number -export function parseConfig(str: string, type: ConfigType): boolean | string | number -export function parseConfig(str: string, type: ConfigType) { - switch(type) { - case ConfigType.Bool: - return str === 'true'; - case ConfigType.String: - return str.trim(); - case ConfigType.Number: { - const n = parseInt(str); - if (isNaN(n)) throw 'Not a valid number'; - return n; - } - } -} - -export function getOptions(type: ConfigType): string[] { - switch(type) { - case ConfigType.Bool: - return ['true', 'false']; - case ConfigType.String: - return []; - case ConfigType.Number: - return []; - } -} - -export function toStringConfig(value: boolean | string | number, type: ConfigType): string { - switch(type) { - case ConfigType.Bool: - return value ? 'true' : 'false'; - case ConfigType.String: - return (value as string); - case ConfigType.Number: - return (value as number).toString(); - } -} - -export const counterConfigs = new Map([ - ['anonymous', { - type: ConfigType.Bool, - default: false - }], - ['messageTemplate', { - type: ConfigType.String, - default: '**%user** has %action the counter by **%amt**.' - }], - ['messageTemplateIncrease', { - type: ConfigType.String, - default: 'null' - }], - ['messageTemplateDecrease', { - type: ConfigType.String, - default: 'null' - }], - - // these ones are fake and are just stand-ins for values defined inside the actual counters table - ['emoji', { - type: ConfigType.String, - default: '' - }] -]); - -export async function updateCounter(bot: Client, counter: Counter, value: number) { - const channel = await bot.channels.fetch(counter.channel) as TextChannel; - const messageID = counter.message; - - const content = `[${counter.emoji}] x${value}`; - - // bit janky - // yeah you don't say - try { - if (messageID) { - const message = await channel.messages.fetch(messageID); - if (!message) throw new Error(); - await message.edit(content); - } else { - throw new Error(); - } - } catch(err) { - const message = await channel.send(content); - message.pin(); - - await db('counters') - .where('id', counter.id) - .update({ - 'message': message.id - }); - } -} - -export async function announceCounterUpdate(bot: Client, member: GuildMember, delta: number, counter: Counter, value: number) { - const channel = await bot.channels.fetch(counter.channel) as TextChannel; - - let template = await getCounterConfig(counter.id, 'messageTemplate') as string; - const templateIncrease = await getCounterConfig(counter.id, 'messageTemplateIncrease') as string; - if (templateIncrease !== 'null' && delta > 0) template = templateIncrease; - const templateDecrease = await getCounterConfig(counter.id, 'messageTemplateDecrease') as string; - if (templateDecrease !== 'null' && delta < 0) template = templateDecrease; - - const anonymous = await getCounterConfig(counter.id, 'anonymous') as boolean; - - const embed = new EmbedBuilder() - //.setDescription(`**${member.toString()}** has ${delta > 0 ? 'increased' : 'decreased'} the counter by **${Math.abs(delta)}**.`) - .setDescription( - template - .replaceAll('%user', anonymous ? 'someone' : member.toString()) - .replaceAll('%action', delta > 0 ? 'increased' : 'decreased') - .replaceAll('%amt', Math.abs(delta).toString()) - .replaceAll('%total', value.toString()) - ) - .setTimestamp() - .setFooter({ - text: `[${counter.emoji}] x${value}` - }); - - if (!anonymous) { - embed - .setAuthor({ - name: member.displayName, - iconURL: member.displayAvatarURL() - }) - .setColor(member.displayColor); - } - - await channel.send({ - embeds: [embed] - }); -} - -export async function changeCounterInteraction(interaction: CommandInteraction, member: GuildMember, amount: number, type: string) { - try { - const counter = await findCounter(type, member.guild.id); - - let canUse = true; - if (amount > 0 && counter.allowlistProducer) { - const userLink = await db('counterUserLink') - .where('id', counter.id) - .where('user', member.id) - .where('producer', true) - .first(); - - if (!userLink) canUse = false; - } - if (amount < 0 && counter.allowlistConsumer) { - const userLink = await db('counterUserLink') - .where('id', counter.id) - .where('user', member.id) - .where('producer', false) - .first(); - - if (!userLink) canUse = false; - } - - if (!canUse) { - await interaction.followUp({ - content: `You cannot **${amount > 0 ? 'produce' : 'consume'}** ${counter.emoji}.` - }); - return; - } - - const newCount = await changeCounter(counter.id, amount); - await updateCounter(interaction.client, counter, newCount); - await announceCounterUpdate(interaction.client, member, amount, counter, newCount); - await interaction.followUp({ - content: `${counter.emoji} **You have ${amount > 0 ? 'increased' : 'decreased'} the counter.**\n\`\`\`diff\n ${newCount - amount}\n${getSign(amount)}${Math.abs(amount)}\n ${newCount}\`\`\`` - }); - } catch(err) { - await interaction.followUp({ - content: (err as Error).toString() - }); - } -} - -export async function counterAutocomplete(interaction: AutocompleteInteraction) { - const focusedValue = interaction.options.getFocused(); - const guild = interaction.guildId; - - const query = db('counters') - .select('emoji', 'key') - .whereLike('key', `%${focusedValue.toLowerCase()}%`) - .limit(25); - - if (guild) { - query.where('guild', guild); - } - - const foundCounters = await query; - - await interaction.respond( - foundCounters.map(choice => ({ name: choice.emoji, value: choice.key })) - ); -} \ No newline at end of file diff --git a/src/lib/db.ts b/src/lib/db.ts index d1ae0ef..e55e449 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -33,7 +33,8 @@ export interface Counter { guild: string, message?: string, allowlistConsumer: boolean, - allowlistProducer: boolean + allowlistProducer: boolean, + linkedItem?: number } export interface CounterUserLink { id: number, @@ -44,4 +45,55 @@ export interface CounterConfiguration { id: number, configName: string, value: string +} +export interface CustomItem { + id: number, + guild: string, + name: string, + description?: string, + emoji: string, + type: 'plain' | 'weapon' | 'consumable', + // also damage for weapons; weapons are always unstackable (cus i said so) + maxStack: number, + untradable: boolean, +} +export interface ItemInventory { + user: string, + item: number, + quantity: number +} +export interface CraftingStationCooldown { + station: string, + user: string, + usedAt: number +} +export interface CustomCraftingRecipe { + id: number, + guild: string, + station: string +} +export interface CustomCraftingRecipeItem { + id: number, + item: number, + quantity: number, + type: 'input' | 'output' | 'requirement' +} +export interface Session { + id: string, + tokenType: string, + accessToken: string, + refreshToken: string, + expiresAt: number, +} +export interface InitHealth { + user: string, +} +export interface InvincibleUser { + user: string, + since: number, +} +export interface ItemBehavior { + item: number, + behavior: string, + value?: number } \ No newline at end of file diff --git a/src/lib/rpg/behaviors.ts b/src/lib/rpg/behaviors.ts new file mode 100644 index 0000000..fd6dc7b --- /dev/null +++ b/src/lib/rpg/behaviors.ts @@ -0,0 +1,106 @@ +import { giveItem, type Item, isDefaultItem, formatItems } from './items'; +import { Either, Left, Right } from '../util'; +import { ItemBehavior, db } from '../db'; +import { BLOOD_ITEM, dealDamage } from './pvp'; + +interface BehaviorContext { + value: number | undefined, +} +type ItemContext = BehaviorContext & { + item: Item, + user: string, +} +type AttackContext = ItemContext & { + target: string, + damage: number, +} + +export interface Behavior { + name: string, + description: string, + type: 'plain' | 'weapon' | 'consumable', + // make it look fancy + format?: (value?: number) => string, + // triggers upon use + // for 'weapons', this is on attack + // for 'consumable' and `plain`, this is on use + // returns Left upon success with an optional message, the reason for failure otherwise (Right) + onUse?: (ctx: ItemContext) => Promise>, + // triggers upon `weapons` attack + // returns the new damage value if applicable upon success and an optional message (Left), the reason for failure otherwise (Right) + onAttack?: (ctx: AttackContext) => Promise>, +} + +const defaultFormat = (behavior: Behavior, value?: number) => `${behavior.name}${value ? ' ' + value.toString() : ''}`; + +export const behaviors: Behavior[] = [ + { + name: 'heal', + description: 'Heals the user by `value`', + type: 'consumable', + async onUse(ctx) { + if (!ctx.value) return new Right('A value is required for this behavior'); + await dealDamage(ctx.user, -Math.floor(ctx.value)); + return new Left(`You were healed by ${formatItems(BLOOD_ITEM, ctx.value)}!`); + }, + }, + { + name: 'damage', + description: 'Damages the user by `value', + type: 'consumable', + async onUse(ctx) { + if (!ctx.value) return new Right('A value is required for this behavior'); + await dealDamage(ctx.user, Math.floor(ctx.value)); + return new Left(`You were damaged by ${formatItems(BLOOD_ITEM, ctx.value)}!`); + }, + }, + { + name: 'random_up', + description: 'Randomizes the attack value up by a maximum of `value`', + type: 'weapon', + format: (value) => `random +${value}`, + async onAttack(ctx) { + if (!ctx.value) return new Right('A value is required for this behavior'); + return new Left({ damage: ctx.damage + Math.round(Math.random() * ctx.value) }); + }, + }, + { + name: 'random_down', + description: 'Randomizes the attack value down by a maximum of `value`', + type: 'weapon', + format: (value) => `random -${value}`, + async onAttack(ctx) { + if (!ctx.value) return new Right('A value is required for this behavior'); + return new Left({ damage: ctx.damage - Math.round(Math.random() * ctx.value) }); + }, + }, + { + name: 'lifesteal', + description: 'Gain blood by stabbing your foes, scaled by `value`x', + type: 'weapon', + format: (value) => `lifesteal ${value}x`, + async onAttack(ctx) { + if (!ctx.value) return new Right('A value is required for this behavior'); + const amt = Math.floor(ctx.damage * ctx.value); + giveItem(ctx.user, BLOOD_ITEM, amt); + return new Left({ + message: `You gained ${formatItems(BLOOD_ITEM, amt)} from your target!` + }); + }, + } +]; + +export async function getBehaviors(item: Item) { + if (isDefaultItem(item)) { + return item.behaviors || []; + } else { + return await db('itemBehaviors') + .where('item', item.id); + } +} +export function getBehavior(behavior: string) { + return behaviors.find(b => b.name === behavior); +} +export function formatBehavior(behavior: Behavior, value: number | undefined) { + return behavior.format ? behavior.format(value) : defaultFormat(behavior, value); +} \ No newline at end of file diff --git a/src/lib/rpg/counter.ts b/src/lib/rpg/counter.ts new file mode 100644 index 0000000..2ea47d8 --- /dev/null +++ b/src/lib/rpg/counter.ts @@ -0,0 +1,406 @@ +import { Client, CommandInteraction, GuildMember, EmbedBuilder, TextChannel, AutocompleteInteraction, User } from 'discord.js'; +import { getSign } from '../util'; +import { Counter, CounterConfiguration, CounterUserLink, db } from '../db'; +import { formatItems, getItem, getItemQuantity, getMaxStack, giveItem } from './items'; +import { resetInvincible } from './pvp'; +import { Autocomplete } from '../autocomplete'; + +export async function getCounter(id: number) { + const counter = await db('counters') + .select('value') + .where('id', id) + .first(); + + if (!counter) throw 'No such counter'; + + return counter.value; +} + +export async function changeCounter(id: number, delta: number) { + const value = await getCounter(id); + const newValue = value + delta; + + await db('counters') + .where('id', id) + .update({ + 'value': newValue + }); + + return newValue; +} + +export async function getCounterData(id: number) { + const counter = await db('counters') + .select('*') + .where('id', id) + .first(); + + if (!counter) throw 'No such counter'; + + return counter; +} + +export async function findCounter(key: string, guild: string) { + const counter = await db('counters') + .select('*') + .where('key', key) + .where('guild', guild) + .first(); + + if (!counter) throw 'No such counter'; + + return counter; +} + +export async function getCounterConfigRaw(counter: Counter) { + const configs = await db('counterConfigurations') + .select('configName', 'value') + .where('id', counter.id); + + const config = new Map(); + configs.forEach(({ configName, value }) => { + config.set(configName, value); + }); + + // just the ugly way of life + config.set('emoji', counter.emoji); + + return config; +} + +export async function getCounterConfig(id: number, key: string) { + const config = await db('counterConfigurations') + .select('value') + .where('id', id) + .where('configName', key) + .first(); + + const valueStr = config?.value; + let value; + if (valueStr) { + value = parseConfig(valueStr, counterConfigs.get(key)!.type); + } else { + value = counterConfigs.get(key)!.default; + } + + return value; +} + +export async function setCounterConfig(counter: Counter, option: string, value: string) { + // just the ugly way of life + if (option === 'emoji' && !counter.linkedItem) { + await db('counters') + .where('id', counter.id) + .update({ + emoji: value + }); + return; + } + + const updated = await db('counterConfigurations') + .update({ + value: value + }) + .where('id', counter.id) + .where('configName', option); + + if (updated === 0) { + await db('counterConfigurations') + .insert({ + id: counter.id, + configName: option, + value: value + }); + } +} + +export enum ConfigType { + Bool, + String, + Number +} + +export function parseConfig(str: string, type: ConfigType.Bool): boolean +export function parseConfig(str: string, type: ConfigType.String): string +export function parseConfig(str: string, type: ConfigType.Number): number +export function parseConfig(str: string, type: ConfigType): boolean | string | number +export function parseConfig(str: string, type: ConfigType) { + switch(type) { + case ConfigType.Bool: + return str === 'true'; + case ConfigType.String: + return str.trim(); + case ConfigType.Number: { + const n = parseInt(str); + if (isNaN(n)) throw 'Not a valid number'; + return n; + } + } +} + +export function getOptions(type: ConfigType): string[] { + switch(type) { + case ConfigType.Bool: + return ['true', 'false']; + case ConfigType.String: + return []; + case ConfigType.Number: + return []; + } +} + +export function toStringConfig(value: boolean | string | number, type: ConfigType): string { + switch(type) { + case ConfigType.Bool: + return value ? 'true' : 'false'; + case ConfigType.String: + return (value as string); + case ConfigType.Number: + return (value as number).toString(); + } +} + +export const counterConfigs = new Map([ + ['anonymous', { + type: ConfigType.Bool, + default: false + }], + ['messageTemplate', { + type: ConfigType.String, + default: '**%user** has %action the counter by **%amt**.' + }], + ['messageTemplateIncrease', { + type: ConfigType.String, + default: 'null' + }], + ['messageTemplateDecrease', { + type: ConfigType.String, + default: 'null' + }], + ['messageTemplateTake', { + type: ConfigType.String, + default: '**%user** has taken **%amt** from the counter.' + }], + ['messageTemplatePut', { + type: ConfigType.String, + default: '**%user** has put **%amt** into the counter.' + }], + ['canIncrement', { + type: ConfigType.Bool, + default: true + }], + ['canDecrement', { + type: ConfigType.Bool, + default: true + }], + ['canPut', { + type: ConfigType.Bool, + default: false + }], + ['canTake', { + type: ConfigType.Bool, + default: false + }], + ['min', { + type: ConfigType.Number, + default: -Number.MIN_SAFE_INTEGER + }], + ['max', { + type: ConfigType.Number, + default: Number.MAX_SAFE_INTEGER + }], + + // these ones are fake and are just stand-ins for values defined inside the actual counters table + ['emoji', { + type: ConfigType.String, + default: '' + }] +]); + +export async function updateCounter(bot: Client, counter: Counter, value: number) { + const channel = await bot.channels.fetch(counter.channel) as TextChannel; + const messageID = counter.message; + + const content = `[${counter.emoji}] x${value}`; + + // bit janky + // yeah you don't say + try { + if (messageID) { + const message = await channel.messages.fetch(messageID); + if (!message) throw new Error(); + await message.edit(content); + } else { + throw new Error(); + } + } catch(err) { + const message = await channel.send(content); + message.pin(); + + await db('counters') + .where('id', counter.id) + .update({ + message: message.id + }); + } +} + +export async function announceCounterUpdate(bot: Client, member: GuildMember, delta: number, counter: Counter, value: number, linked: boolean = false) { + const channel = await bot.channels.fetch(counter.channel) as TextChannel; + + let template = await getCounterConfig(counter.id, 'messageTemplate') as string; + const templateIncrease = await getCounterConfig(counter.id, 'messageTemplateIncrease') as string; + if (templateIncrease !== 'null' && delta > 0) template = templateIncrease; + const templateDecrease = await getCounterConfig(counter.id, 'messageTemplateDecrease') as string; + if (templateDecrease !== 'null' && delta < 0) template = templateDecrease; + const templatePut = await getCounterConfig(counter.id, 'messageTemplatePut') as string; + if (templatePut !== 'null' && delta > 0 && linked) template = templatePut; + const templateTake = await getCounterConfig(counter.id, 'messageTemplateTake') as string; + if (templateTake !== 'null' && delta < 0 && linked) template = templateTake; + + const anonymous = await getCounterConfig(counter.id, 'anonymous') as boolean; + + const embed = new EmbedBuilder() + //.setDescription(`**${member.toString()}** has ${delta > 0 ? 'increased' : 'decreased'} the counter by **${Math.abs(delta)}**.`) + .setDescription( + template + .replaceAll('%user', anonymous ? 'someone' : member.toString()) + .replaceAll('%action', delta > 0 ? (linked ? 'put into' : 'increased') : (linked ? 'taken from' : 'decreased')) + .replaceAll('%amt', Math.abs(delta).toString()) + .replaceAll('%total', value.toString()) + ) + .setTimestamp() + .setFooter({ + text: `[${counter.emoji}] x${value}` + }); + + if (!anonymous) { + embed + .setAuthor({ + name: member.displayName, + iconURL: member.displayAvatarURL() + }) + .setColor(member.displayColor); + } + + await channel.send({ + embeds: [embed] + }); +} + +async function canUseCounter(user: User, counter: Counter, amount: number, isLinkedAction = false): Promise { + if (amount > 0 && !(await getCounterConfig(counter.id, isLinkedAction ? 'canPut' : 'canIncrement') as boolean)) return false; + if (amount > 0 && counter.allowlistProducer) { + const userLink = await db('counterUserLink') + .where('id', counter.id) + .where('user', user.id) + .where('producer', true) + .first(); + + if (!userLink) return false; + } + if (amount < 0 && !(await getCounterConfig(counter.id, isLinkedAction ? 'canTake' : 'canDecrement') as boolean)) return false; + if (amount < 0 && counter.allowlistConsumer) { + const userLink = await db('counterUserLink') + .where('id', counter.id) + .where('user', user.id) + .where('producer', false) + .first(); + + if (!userLink) return false; + } + + return true; +} + +function changeCounterInteractionBuilder(linked: boolean) { + return async (interaction: CommandInteraction, member: GuildMember, amount: number, type: string) => { + try { + const counter = await findCounter(type, member.guild.id); + if (linked && !counter.linkedItem) return interaction.followUp('There is no such linked counter!'); + + const canUse = await canUseCounter(member.user, counter, amount, linked); + + if (!canUse) { + return interaction.followUp(`You cannot **${amount > 0 ? (linked ? 'put' : 'produce') : (linked ? 'take' : 'consume')}** ${counter.emoji}.`); + } + + const min = await getCounterConfig(counter.id, 'min') as number; + const max = await getCounterConfig(counter.id, 'max') as number; + if (counter.value + amount < min) { + if (min === 0) { + return interaction.followUp(`You cannot remove more than how much is in the counter (${counter.value}x ${counter.emoji})!`); + } else { + return interaction.followUp(`You cannot decrement past the minimum value (${min})!`); + } + } + if (counter.value + amount > max) { + return interaction.followUp(`You are adding more than the counter can hold (${max}x)!`); + } + + let item; + let newInv; + if (linked) { + const inv = await getItemQuantity(member.id, counter.linkedItem!); + item = (await getItem(counter.linkedItem!))!; + + // change counter by -10 = increment own counter by 10 + const amtInv = -amount; + const amtAbs = Math.abs(amtInv); + + if (amtInv > getMaxStack(item)) { + return interaction.followUp(`You cannot take ${formatItems(item, amtAbs)}, because the max stack size is ${getMaxStack(item)}x!`); + } + if ((inv.quantity + amtInv) > getMaxStack(item)) { + return interaction.followUp(`You cannot take ${formatItems(item, amtAbs)}, because the max stack size is ${getMaxStack(item)}x and you already have ${inv.quantity}x!`); + } + if ((inv.quantity + amtInv) < 0) { + return interaction.followUp(`You cannot put in ${formatItems(item, amtAbs)}, as you only have ${formatItems(item, inv.quantity)}!`); + } + + newInv = await giveItem(member.id, item, amtInv); + } + + await resetInvincible(member.id); + const newCount = await changeCounter(counter.id, amount); + await updateCounter(interaction.client, counter, newCount); + await announceCounterUpdate(interaction.client, member, amount, counter, newCount, linked); + await interaction.followUp({ + content: `${counter.emoji} **You have ${amount > 0 ? (linked ? 'put into' : 'increased') : (linked ? 'taken from' : 'decreased')} the counter.**\n\`\`\`diff\n ${newCount - amount}\n${getSign(amount)}${Math.abs(amount)}\n ${newCount}\`\`\`${newInv ? `\nYou now have ${formatItems(item, newInv.quantity)}.` : ''}` + }); + } catch(err) { + await interaction.followUp({ + content: (err as Error).toString() + }); + } + }; +} + +export const changeCounterInteraction = changeCounterInteractionBuilder(false); +export const changeLinkedCounterInteraction = changeCounterInteractionBuilder(true); + +function counterAutocompleteBuilder(linked: boolean): Autocomplete { + return async (interaction: AutocompleteInteraction) => { + const focusedValue = interaction.options.getFocused(); + const guild = interaction.guildId; + + const query = db('counters') + .select('emoji', 'key') + .whereLike('key', `%${focusedValue.toLowerCase()}%`) + .limit(25); + + if (guild) { + query.where('guild', guild); + } + if (linked) { + query.whereNotNull('linkedItem'); + } + + const foundCounters = await query; + + return foundCounters.map(choice => ({ name: `${choice.emoji} ${choice.key}`, value: choice.key })); + }; +} + +export const counterAutocomplete = counterAutocompleteBuilder(false); +export const linkedCounterAutocomplete = counterAutocompleteBuilder(true); \ No newline at end of file diff --git a/src/lib/rpg/craftingStations.ts b/src/lib/rpg/craftingStations.ts new file mode 100644 index 0000000..e95348d --- /dev/null +++ b/src/lib/rpg/craftingStations.ts @@ -0,0 +1,127 @@ +import { pickRandom } from '../util'; +import { DefaultItems, Item, Items, formatItem, formatItems, formatItemsArray, getDefaultItem, getItemQuantity } from './items'; + +export interface CraftingStation { + key: string, + name: string, + verb?: string, + description: string, + emoji: string, + requires?: Item, + // in seconds + cooldown?: number, + formatRecipe?: (inputs: Items[], requirements: Items[], outputs: Items[], disableBold?: boolean) => string, + manipulateResults?: (outputs: Items[]) => Items[] +} + +export function getStation(key: string) { + return craftingStations.find(station => station.key === key); +} + +export const defaultVerb = 'Crafted'; + +const rollBunch = (outputs: Items[]) => { + const totalItems = outputs.reduce((a, b) => a + b.quantity, 0); + // grab from 1/3 to the entire pool, ensure it never goes below 1 + const rolledItems = Math.max(Math.round(totalItems/3 + Math.random() * totalItems*2/3), 1); + const res: Items[] = []; + for (let i = 0; i < rolledItems; i++) { + const rolled = pickRandom(outputs); + const r = res.find(r => r.item.id === rolled.item.id); + if (r) { + if (r.quantity === rolled.quantity) { + // don't roll more than can be rolled + i--; + } else { + r.quantity = r.quantity + 1; + } + } else { + res.push({ item: rolled.item, quantity: 1 }); + } + } + return res; +}; + +const formatMaybeCountable = (inputs: Items[], requirements: Items[], outputs: Items[], disableBold = false) => + `${inputs.length > 0 ? formatItemsArray(inputs, disableBold) : ''} ${requirements.length > 0 ? `w/ ${formatItemsArray(requirements, disableBold)}: ` : ''}${outputs.map(i => formatItems(i.item, i.quantity, disableBold) + '?').join(' ')}`; + +const formatMaybe = (inputs: Items[], requirements: Items[], outputs: Items[], disableBold = false) => + `${inputs.length > 0 ? formatItemsArray(inputs, disableBold) : ''} ${requirements.length > 0 ? `w/ ${formatItemsArray(requirements, disableBold)} ` : ''}=> ${outputs.map(i => formatItem(i.item, disableBold) + '?').join(' ')}`; + +export const craftingStations: CraftingStation[] = [ + { + key: 'forage', + name: 'Forage', + verb: 'Foraged', + description: 'Pick up various sticks and stones from the forest', + emoji: '🌲', + cooldown: 60 * 5, + formatRecipe: formatMaybeCountable, + manipulateResults: rollBunch + }, + { + key: 'hand', + name: 'Hand', + verb: 'Made', + description: 'You can use your hands to make a small assortment of things', + emoji: 'βœ‹' + }, + { + key: 'workbench', + name: 'Workbench', + description: 'A place for you to work with tools, for simple things', + emoji: 'πŸ› οΈ', + requires: getDefaultItem(DefaultItems.WORKBENCH) + }, + { + key: 'fishing', + name: 'Fishing', + verb: 'Fished up', + description: 'fish gaming wednesday', + emoji: '🎣', + cooldown: 60 * 60 * 2, + requires: getDefaultItem(DefaultItems.FISHING_ROD), + formatRecipe: formatMaybe, + // weighted random + manipulateResults: (outputs) => { + const pool: Item[] = []; + for (const out of outputs) { + for (let i = 0; i < out.quantity; i++) { + pool.push(out.item); + } + } + return [{ item: pickRandom(pool), quantity: 1 }]; + } + }, + { + key: 'mining', + name: 'Mining', + verb: 'Mined', + description: 'mine diamonds', + emoji: '⛏️', + cooldown: 60 * 30, + requires: getDefaultItem(DefaultItems.MINESHAFT), + formatRecipe: formatMaybeCountable, + manipulateResults: rollBunch, + }, + { + key: 'smelting', + name: 'Smelting', + verb: 'Smelt', + description: 'Smelt ores, minerals, food, whatever you please', + emoji: 'πŸ”₯', + cooldown: 30, + requires: getDefaultItem(DefaultItems.FURNACE), + }, +]; + +export async function canUseStation(user: string, station: CraftingStation) { + if (!station.requires) return true; + + const inv = await getItemQuantity(user, station.requires.id); + return inv.quantity > 0; +} + +export function verb(station: CraftingStation) { + return station.verb || defaultVerb; +} \ No newline at end of file diff --git a/src/lib/rpg/items.ts b/src/lib/rpg/items.ts new file mode 100644 index 0000000..9dc1442 --- /dev/null +++ b/src/lib/rpg/items.ts @@ -0,0 +1,427 @@ +import { AutocompleteInteraction } from 'discord.js'; +import { CustomItem, ItemBehavior, ItemInventory, db } from '../db'; +import { Autocomplete } from '../autocomplete'; + +// uses negative IDs +export type DefaultItem = Omit & { behaviors?: Omit[] }; +export type Item = DefaultItem | CustomItem; + +export interface Items { + item: Item, + quantity: number +} + +export enum DefaultItems { + COIN = 1, + WORKBENCH = 2, + PEBBLE = 3, + TWIG = 4, + APPLE = 5, + BERRIES = 6, + LOG = 7, + AXE = 8, + BLOOD = 9, + BAIT = 10, + FISHING_ROD = 11, + CARP = 12, + PUFFERFISH = 13, + EXOTIC_FISH = 14, + SHOVEL = 15, + DIRT = 16, + MINESHAFT = 17, + PICKAXE = 18, + IRON_PICKAXE = 19, + COAL = 20, + IRON_ORE = 21, + IRON_INGOT = 22, + DIAMOND = 23, + RUBY = 24, + EMERALD = 25, + FURNACE = 26, + FRIED_FISH = 27, +} + +export const defaultItems: DefaultItem[] = [ + { + id: -1, + name: 'Coin', + emoji: 'πŸͺ™', + type: 'plain', + maxStack: 9999, + untradable: false + }, + { + id: -2, + name: 'Workbench', + description: 'A place for you to work with tools, for simple things', + emoji: 'πŸ› οΈ', + type: 'plain', + maxStack: 1, + untradable: false + }, + { + id: -3, + name: 'Pebble', + description: 'If you get 5 of them you will instantly ! !!!', + emoji: 'πŸͺ¨', + type: 'plain', + maxStack: 64, + untradable: false + }, + { + id: -4, + name: 'Twig', + description: 'Just a tiny bit of wood', + emoji: '🌿', + type: 'plain', + maxStack: 64, + untradable: false + }, + { + id: -5, + name: 'Apple', + description: 'A forager\'s snack', + emoji: '🍎', + type: 'consumable', + maxStack: 16, + untradable: false, + behaviors: [{ behavior: 'heal', value: 10 }], + }, + { + id: -6, + name: 'Berries', + description: 'A little treat for the road!', + emoji: 'πŸ“', + type: 'consumable', + maxStack: 16, + untradable: false, + behaviors: [{ behavior: 'heal', value: 4 }], + }, + { + id: -7, + name: 'Log', + description: '㏒', + emoji: 'πŸͺ΅', + type: 'plain', + maxStack: 64, + untradable: false + }, + { + id: -8, + name: 'Axe', + description: 'You could chop trees with this. Or commit murder! The choice is up to you', + emoji: 'πŸͺ“', + type: 'weapon', + maxStack: 1, + untradable: false + }, + { + id: -9, + name: 'Blood', + description: 'ow', + emoji: '🩸', + type: 'plain', + maxStack: 50, + untradable: false + }, + { + id: -10, + name: 'Bait', + description: 'I guess you could eat this.', + emoji: 'πŸͺ±', + type: 'consumable', + maxStack: 128, + untradable: false, + behaviors: [{ behavior: 'heal', value: 1 }], + }, + { + id: -11, + name: 'Fishing Rod', + description: 'Give a man a fish, and he will eat for a day', + emoji: '🎣', + type: 'plain', + maxStack: 1, + untradable: false + }, + { + id: -12, + name: 'Carp', + description: 'wow', + emoji: '🐟️', + type: 'plain', + maxStack: 16, + untradable: false + }, + { + id: -13, + name: 'Pufferfish', + description: 'yummy!', + emoji: '🐑', + type: 'plain', + maxStack: 16, + untradable: false + }, + { + id: -14, + name: 'Exotic Fish', + description: 'lucky!', + emoji: '🐠', + type: 'plain', + maxStack: 16, + untradable: false, + }, + { + id: -15, + name: 'Shovel', + description: 'Did you know there\'s no shovel emoji', + emoji: '♠️', + type: 'plain', + maxStack: 1, + untradable: false, + }, + { + id: -16, + name: 'Dirt', + description: 'https://media.discordapp.net/attachments/819472665291128873/1081454188325785650/ezgif-2-5ccc7dedf8.gif', + emoji: '🟫', + type: 'consumable', + maxStack: 64, + untradable: false, + }, + { + id: -17, + name: 'Mineshaft', + description: 'A place for you to mine ores and minerals!', + emoji: '⛏️', + type: 'plain', + maxStack: 1, + untradable: true + }, + { + id: -18, + name: 'Pickaxe', + description: 'Basic mining equipment', + emoji: '⛏️', + type: 'plain', + maxStack: 8, + untradable: false + }, + { + id: -19, + name: 'Iron Pickaxe', + description: 'More durable and strong mining equipment', + emoji: 'βš’οΈ', + type: 'plain', + maxStack: 8, + untradable: false + }, + { + id: -20, + name: 'Coal', + description: 'Fuel, NOT EDIBLE', + emoji: '◾️', + type: 'plain', + maxStack: 128, + untradable: false + }, + { + id: -21, + name: 'Iron Ore', + description: 'Unsmelted iron', + emoji: '◽️', + type: 'plain', + maxStack: 128, + untradable: false + }, + { + id: -22, + name: 'Iron Ingot', + description: 'A sturdy material', + emoji: '◻️', + type: 'plain', + maxStack: 128, + untradable: false + }, + { + id: -23, + name: 'Diamond', + description: 'Shiny rock!', + emoji: 'πŸ’Ž', + type: 'plain', + maxStack: 128, + untradable: false + }, + { + id: -24, + name: 'Ruby', + description: 'Reference to the progarmiing......g.', + emoji: 'πŸ”»', + type: 'plain', + maxStack: 128, + untradable: false + }, + { + id: -25, + name: 'Emerald', + description: 'A currency in some other world', + emoji: '🟩', + type: 'plain', + maxStack: 128, + untradable: false + }, + { + id: -26, + name: 'Furnace', + description: 'A smeltery for your own needs', + emoji: 'πŸ”₯', + type: 'plain', + maxStack: 1, + untradable: false + }, + { + id: -27, + name: 'Fried Fish', + description: 'A very nice and filling meal', + emoji: '🍱', + type: 'consumable', + maxStack: 16, + untradable: false, + behaviors: [{ behavior: 'heal', value: 35 }], + }, +]; + + +export function getDefaultItem(id: DefaultItems): Item +export function getDefaultItem(id: number): Item | undefined { + return defaultItems.find(item => Math.abs(item.id) === Math.abs(id)); +} + +export async function getItem(id: number): Promise { + if (id >= 0) { + return await getCustomItem(id); + } else { + return getDefaultItem(id); + } +} + +export async function getCustomItem(id: number) { + return await db('customItems') + .where('id', id) + .first(); +} + +export async function getItemQuantity(user: string, itemID: number): Promise { + return (await db('itemInventories') + .where('item', itemID) + .where('user', user) + .first()) + || { + user: user, + item: itemID, + quantity: 0 + }; +} + +export async function giveItem(user: string, item: Item, quantity = 1): Promise { + const storedItem = await db('itemInventories') + .where('user', user) + .where('item', item.id) + .first(); + + let inv; + if (storedItem) { + if (storedItem.quantity + quantity === 0 && item.id !== DefaultItems.BLOOD) { // let blood show as 0x + await db('itemInventories') + .delete() + .where('user', user) + .where('item', item.id); + return { + user: user, + item: item.id, + quantity: 0 + }; + } + + inv = await db('itemInventories') + .update({ + quantity: db.raw('MIN(quantity + ?, ?)', [quantity, getMaxStack(item)]) + }) + .limit(1) + .where('user', user) + .where('item', item.id) + .returning('*'); + } else { + inv = await db('itemInventories') + .insert({ + user: user, + item: item.id, + quantity: Math.min(quantity, getMaxStack(item)), + }) + .returning('*'); + } + + return inv[0]; +} + +export function getMaxStack(item: Item) { + return item.type === 'weapon' ? 1 : item.maxStack; +} + +export function isDefaultItem(item: Item): item is DefaultItem { + return item.id < 0; +} + +export function formatItem(item: Item | undefined, disableBold = false) { + if (!item) return disableBold ? '? MISSINGNO' : '? **MISSINGNO**'; + return disableBold ? `${item.emoji} ${item.name}` : `${item.emoji} **${item.name}**`; +} +export function formatItems(item: Item | undefined, quantity: number, disableBold = false) { + return `${quantity}x ${formatItem(item, disableBold)}`; +} +export function formatItemsArray(items: Items[], disableBold = false) { + if (items.length === 0) return disableBold ? 'nothing' : '**nothing**'; + return items.map(i => formatItems(i.item, i.quantity, disableBold)).join(' '); +} + +function createItemAutocomplete(onlyCustom: boolean, filterType: 'plain' | 'weapon' | 'consumable' | null, inventory: boolean = false): Autocomplete { + return async (interaction: AutocompleteInteraction) => { + const focused = interaction.options.getFocused(); + + const itemQuery = db('customItems') + .select('emoji', 'name', 'id') + // @ts-expect-error this LITERALLY works + .whereLike(db.raw('UPPER(name)'), `%${focused.toUpperCase()}%`) + .where('guild', interaction.guildId!) + .limit(25); + + if (filterType) itemQuery.where('type', filterType); + if (inventory) itemQuery + .innerJoin('itemInventories', 'itemInventories.item', '=', 'customItems.id') + .where('itemInventories.user', '=', interaction.member!.user.id) + .where('itemInventories.quantity', '>', '0'); + + const customItems = await itemQuery; + + let foundDefaultItems = defaultItems.filter(item => item.name.toUpperCase().includes(focused.toUpperCase())); + if (filterType) foundDefaultItems = foundDefaultItems.filter(i => i.type === filterType); + if (inventory) foundDefaultItems = (await Promise.all(foundDefaultItems.map(async i => ({...i, inv: await getItemQuantity(interaction.member!.user.id, i.id)})))).filter(i => i.inv.quantity > 0); + + let items; + if (onlyCustom) { + items = customItems; + } else { + items = [...foundDefaultItems, ...customItems]; + } + + return items.map(choice => ({ name: `${choice.emoji} ${choice.name}`, value: choice.id.toString() })); + }; +} + +export const itemAutocomplete = createItemAutocomplete(false, null); +export const customItemAutocomplete = createItemAutocomplete(true, null); +export const plainAutocomplete = createItemAutocomplete(false, 'plain'); +export const weaponAutocomplete = createItemAutocomplete(false, 'weapon'); +export const consumableAutocomplete = createItemAutocomplete(false, 'consumable'); +export const plainInventoryAutocomplete = createItemAutocomplete(false, 'plain', true); +export const weaponInventoryAutocomplete = createItemAutocomplete(false, 'weapon', true); +export const consumableInventoryAutocomplete = createItemAutocomplete(false, 'consumable', true); \ No newline at end of file diff --git a/src/lib/rpg/pvp.ts b/src/lib/rpg/pvp.ts new file mode 100644 index 0000000..62bdf3c --- /dev/null +++ b/src/lib/rpg/pvp.ts @@ -0,0 +1,83 @@ +import { InitHealth, InvincibleUser, ItemInventory, db } from '../db'; +import { DefaultItems, getDefaultItem, giveItem, getItemQuantity, formatItems } from './items'; +import { Client } from 'discord.js'; + +export const BLOOD_ID = -DefaultItems.BLOOD; +export const BLOOD_ITEM = getDefaultItem(BLOOD_ID); +export const MAX_HEALTH = BLOOD_ITEM.maxStack; +const BLOOD_GAIN_PER_HOUR = 2; +export const INVINCIBLE_TIMER = 1_000 * 60 * 30; + +export async function initHealth(user: string) { + const isInitialized = await db('initHealth') + .where('user', user) + .first(); + + if (!isInitialized) { + giveItem(user, BLOOD_ITEM, MAX_HEALTH); + await db('initHealth').insert({ user }); + } +} + +export async function getInvincibleMs(user: string) { + const invincible = await db('invincibleUsers') + .where('user', user) + .first(); + + if (!invincible) return 0; + return Math.max((invincible.since + INVINCIBLE_TIMER) - Date.now(), 0); +} + +export async function resetInvincible(user: string) { + await db('invincibleUsers') + .where('user', user) + .delete(); +} + +export async function applyInvincible(user: string) { + const exists = await db('invincibleUsers') + .where('user', user) + .first(); + + if (exists) { + await db('invincibleUsers') + .update({ since: Date.now() }); + } else { + await db('invincibleUsers') + .insert({ since: Date.now(), user }); + } +} + +export async function getHealth(user: string) { + await initHealth(user); + return await getItemQuantity(user, BLOOD_ID); +} + +export async function dealDamage(user: string, dmg: number) { + await initHealth(user); + await applyInvincible(user); + return await giveItem(user, BLOOD_ITEM, -dmg); +} + +async function healthCron(bot: Client) { + await db('itemInventories') + .where('item', BLOOD_ID) + .update({ + quantity: db.raw('MIN(quantity + ?, ?)', [BLOOD_GAIN_PER_HOUR, MAX_HEALTH]) + }); + + const debtedUsers = await db('itemInventories') + .select('user', 'quantity') + .where('quantity', '<', '0'); + + for (const debted of debtedUsers) { + const user = await bot.users.fetch(debted.user); + if (!user) continue; + await user.send(`${formatItems(BLOOD_ITEM, debted.quantity)} You are bleeding out to death`); + } +} + +export function init(bot: Client) { + healthCron(bot); + setInterval(() => healthCron(bot), 1_000 * 60 * 60); +} \ No newline at end of file diff --git a/src/lib/rpg/recipes.ts b/src/lib/rpg/recipes.ts new file mode 100644 index 0000000..9dba031 --- /dev/null +++ b/src/lib/rpg/recipes.ts @@ -0,0 +1,301 @@ +import { CustomCraftingRecipe, CustomCraftingRecipeItem, db } from '../db'; +import { getStation } from './craftingStations'; +import { DefaultItems, Items, formatItemsArray, getDefaultItem, getItem } from './items'; + +export interface DefaultRecipe { + id: number, + station: string, + inputs: Items[], + requirements: Items[], + outputs: Items[] +} +export interface ResolvedCustomRecipe { + id: number, + guild: string, + station: string, + inputs: Items[], + requirements: Items[], + outputs: Items[] +} +export type Recipe = DefaultRecipe | ResolvedCustomRecipe + +export const defaultRecipes: DefaultRecipe[] = [ + { + id: -1, + station: 'forage', + inputs: [], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.PEBBLE), quantity: 4 }, + { item: getDefaultItem(DefaultItems.TWIG), quantity: 2 }, + { item: getDefaultItem(DefaultItems.BERRIES), quantity: 2 } + ] + }, + { + id: -2, + station: 'hand', + inputs: [ + { item: getDefaultItem(DefaultItems.PEBBLE), quantity: 2 }, + { item: getDefaultItem(DefaultItems.TWIG), quantity: 2 }, + ], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.WORKBENCH), quantity: 1 } + ] + }, + { + id: -3, + station: 'forage', + inputs: [], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.PEBBLE), quantity: 2 }, + { item: getDefaultItem(DefaultItems.TWIG), quantity: 4 }, + { item: getDefaultItem(DefaultItems.APPLE), quantity: 1 } + ] + }, + { + id: -4, + station: 'forage', + inputs: [], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.TWIG), quantity: 1 }, + { item: getDefaultItem(DefaultItems.COIN), quantity: 1 }, + { item: getDefaultItem(DefaultItems.APPLE), quantity: 4 }, + { item: getDefaultItem(DefaultItems.BERRIES), quantity: 6 }, + ] + }, + { + id: -5, + station: 'forage', + inputs: [], + requirements: [ + { item: getDefaultItem(DefaultItems.AXE), quantity: 1 }, + ], + outputs: [ + { item: getDefaultItem(DefaultItems.TWIG), quantity: 1 }, + { item: getDefaultItem(DefaultItems.LOG), quantity: 3 }, + ] + }, + { + id: -6, + station: 'workbench', + inputs: [ + { item: getDefaultItem(DefaultItems.PEBBLE), quantity: 4 }, + { item: getDefaultItem(DefaultItems.TWIG), quantity: 2 }, + ], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.AXE), quantity: 1 }, + ] + }, + { + id: -7, + station: 'forage', + inputs: [], + requirements: [ + { item: getDefaultItem(DefaultItems.AXE), quantity: 1 }, + ], + outputs: [ + { item: getDefaultItem(DefaultItems.BLOOD), quantity: 6 }, + ] + }, + { + id: -8, + station: 'fishing', + inputs: [ + { item: getDefaultItem(DefaultItems.BAIT), quantity: 1 } + ], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.CARP), quantity: 12 }, + { item: getDefaultItem(DefaultItems.PUFFERFISH), quantity: 4 }, + { item: getDefaultItem(DefaultItems.EXOTIC_FISH), quantity: 1 }, + ] + }, + { + id: -9, + station: 'workbench', + inputs: [ + { item: getDefaultItem(DefaultItems.TWIG), quantity: 2 }, + { item: getDefaultItem(DefaultItems.BAIT), quantity: 1 }, + ], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.FISHING_ROD), quantity: 1 } + ] + }, + { + id: -10, + station: 'forage', + inputs: [], + requirements: [ + { item: getDefaultItem(DefaultItems.SHOVEL), quantity: 1 }, + ], + outputs: [ + { item: getDefaultItem(DefaultItems.BAIT), quantity: 4 }, + { item: getDefaultItem(DefaultItems.PEBBLE), quantity: 1 }, + { item: getDefaultItem(DefaultItems.DIRT), quantity: 3 }, + ], + }, + { + id: -11, + station: 'workbench', + inputs: [ + { item: getDefaultItem(DefaultItems.PEBBLE), quantity: 3 }, + { item: getDefaultItem(DefaultItems.TWIG), quantity: 2 }, + ], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.SHOVEL), quantity: 1 }, + ] + }, + { + id: -12, + station: 'hand', + inputs: [ + { item: getDefaultItem(DefaultItems.DIRT), quantity: 4 }, + { item: getDefaultItem(DefaultItems.PEBBLE), quantity: 4 }, + ], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.MINESHAFT), quantity: 1 }, + ] + }, + { + id: -13, + station: 'mining', + inputs: [ + { item: getDefaultItem(DefaultItems.PICKAXE), quantity: 1 }, + ], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.PEBBLE), quantity: 10 }, + { item: getDefaultItem(DefaultItems.COAL), quantity: 5 }, + { item: getDefaultItem(DefaultItems.IRON_ORE), quantity: 5 }, + ] + }, + { + id: -14, + station: 'mining', + inputs: [ + { item: getDefaultItem(DefaultItems.IRON_PICKAXE), quantity: 1 }, + ], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.PEBBLE), quantity: 10 }, + { item: getDefaultItem(DefaultItems.COAL), quantity: 5 }, + { item: getDefaultItem(DefaultItems.IRON_ORE), quantity: 5 }, + { item: getDefaultItem(DefaultItems.DIAMOND), quantity: 1 }, + { item: getDefaultItem(DefaultItems.EMERALD), quantity: 1 }, + { item: getDefaultItem(DefaultItems.RUBY), quantity: 1 }, + ] + }, + { + id: -15, + station: 'workbench', + inputs: [ + { item: getDefaultItem(DefaultItems.PEBBLE), quantity: 4 }, + { item: getDefaultItem(DefaultItems.TWIG), quantity: 3 }, + ], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.PICKAXE), quantity: 1 }, + ] + }, + { + id: -16, + station: 'workbench', + inputs: [ + { item: getDefaultItem(DefaultItems.IRON_INGOT), quantity: 4 }, + { item: getDefaultItem(DefaultItems.TWIG), quantity: 3 }, + ], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.IRON_PICKAXE), quantity: 1 }, + ] + }, + { + id: -17, + station: 'smelting', + inputs: [ + { item: getDefaultItem(DefaultItems.IRON_ORE), quantity: 2 }, + { item: getDefaultItem(DefaultItems.COAL), quantity: 1 }, + ], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.IRON_INGOT), quantity: 1 }, + ] + }, + { + id: -18, + station: 'smelting', + inputs: [ + { item: getDefaultItem(DefaultItems.CARP), quantity: 1 }, + { item: getDefaultItem(DefaultItems.COAL), quantity: 1 }, + ], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.FRIED_FISH), quantity: 1 }, + ] + }, + { + id: -19, + station: 'workbench', + inputs: [ + { item: getDefaultItem(DefaultItems.PEBBLE), quantity: 4 }, + { item: getDefaultItem(DefaultItems.COAL), quantity: 1 }, + ], + requirements: [], + outputs: [ + { item: getDefaultItem(DefaultItems.FURNACE), quantity: 1 }, + ] + } +]; + +export function getDefaultRecipe(id: number): DefaultRecipe | undefined { + return defaultRecipes.find(recipe => recipe.id === id); +} +export async function getCustomRecipe(id: number): Promise { + const recipe = await db('customCraftingRecipes') + .where('id', id) + .first(); + + if (!recipe) return; + + return await resolveCustomRecipe(recipe); +} +export async function getRecipe(id: number): Promise { + if (id >= 0) { + return await getCustomRecipe(id); + } else { + return getDefaultRecipe(id); + } +} + +const defaultFormatRecipe = (inputs: Items[], requirements: Items[], outputs: Items[], disableBold = false) => + `${formatItemsArray(inputs, disableBold)}${requirements.length === 0 ? '' : ` w/ ${formatItemsArray(requirements, disableBold)}`} => ${formatItemsArray(outputs, disableBold)}`; + +export function formatRecipe(recipe: Recipe, disableBold = false) { + const station = getStation(recipe.station); + return (station?.formatRecipe || defaultFormatRecipe)(recipe.inputs, recipe.requirements, recipe.outputs, disableBold); +} + +function resolveItems(items: CustomCraftingRecipeItem[]) { + return Promise.all(items.map(async i => ({item: (await getItem(i.item))!, quantity: i.quantity}))); +} + +export async function resolveCustomRecipe(recipe: CustomCraftingRecipe): Promise { + const items = await db('customCraftingRecipeItems') + .where('id', recipe.id); + + return { + id: recipe.id, + guild: recipe.guild, + station: recipe.station, + inputs: await resolveItems(items.filter(i => i.type === 'input')), + requirements: await resolveItems(items.filter(i => i.type === 'requirement')), + outputs: await resolveItems(items.filter(i => i.type === 'output')), + }; +} \ No newline at end of file diff --git a/src/lib/util.ts b/src/lib/util.ts index 8803979..d822174 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -23,4 +23,21 @@ export async function writeTmpFile(data: string | Buffer, filename?: string, ext const path = join(tmpdir(), file); await fsp.writeFile(path, data); return path; -} \ No newline at end of file +} + +export function pickRandom(list: T[]): T { + return list[Math.floor(Math.random() * list.length)]; +} + +// WE OUT HERE +export type Either = Left | Right + +export class Left { + constructor(private readonly value: L) {} + public getValue() { return this.value; } +} + +export class Right { + constructor(private readonly value: R) {} + public getValue() { return this.value; } +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 79ef120..293edc7 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,7 +1,25 @@ -import { Collection } from 'discord.js'; +import { Collection, SlashCommandBuilder, CommandInteraction, Client } from 'discord.js'; +import { Autocomplete } from '../lib/autocomplete'; + +export interface Command { + data: Pick, + execute: (interaction: CommandInteraction) => Promise, + autocomplete?: Autocomplete, + onClientReady?: (client: Client) => Promise, + serverWhitelist?: string[], +} + +export interface Config { + token: string, + sitePort: number, + siteURL: string, + clientId: string, + clientSecret: string, +} declare module 'discord.js' { export interface Client { - commands: Collection; + config: Config, + commands: Collection; } } \ No newline at end of file diff --git a/src/web/oauth2.ts b/src/web/oauth2.ts new file mode 100644 index 0000000..45ebcc3 --- /dev/null +++ b/src/web/oauth2.ts @@ -0,0 +1,117 @@ +import type { Response } from 'express'; +import type { IncomingHttpHeaders } from 'http'; +import * as log from '../lib/log'; +import { Cookie, parse as parseCookie } from 'tough-cookie'; +import uid from 'uid-safe'; +import { Client, RESTPostOAuth2AccessTokenResult, RESTPostOAuth2AccessTokenURLEncodedData, RESTPostOAuth2RefreshTokenURLEncodedData, Routes } from 'discord.js'; +import got from 'got'; +import { Session, db } from '../lib/db'; + +export const DISCORD_ENDPOINT = 'https://discord.com/api/v10'; +const UID_BYTE_LENGTH = 18; +const UID_STRING_LENGTH = 24; // why? +const COOKIE_KEY = 'PHPSESSID'; +const COOKIE_EXPIRES = 1_000 * 60 * 60 * 24 * 365; + +export async function getSessionString(cookieStr: string) { + const cookies = cookieStr.split('; ').map(s => parseCookie(s)!).filter(c => c !== null); + const sessionCookie = cookies.find(c => c.key === COOKIE_KEY); + + if (!sessionCookie || sessionCookie.value.length !== UID_STRING_LENGTH) { + return await uid(UID_BYTE_LENGTH); + } else { + return sessionCookie.value; + } +} + +export function updateCookie(res: Response, sessionId: string) { + const cookie = new Cookie({ + key: COOKIE_KEY, + value: sessionId, + expires: new Date(Date.now() + COOKIE_EXPIRES), + sameSite: 'strict' + }); + res.setHeader('Set-Cookie', cookie.toString()); +} + +export async function getToken(bot: Client, code: string) { + try { + return await got.post(DISCORD_ENDPOINT + Routes.oauth2TokenExchange(), { + form: { + client_id: bot.config.clientId, + client_secret: bot.config.clientSecret, + code, + grant_type: 'authorization_code', + redirect_uri: bot.config.siteURL, + } satisfies RESTPostOAuth2AccessTokenURLEncodedData + // if you're looking to change this then you are blissfully unaware of the past + // and have learnt 0 lessons + }).json() as RESTPostOAuth2AccessTokenResult; + } catch(err) { + log.error(err); + return; + } +} + +async function refreshToken(bot: Client, sessionId: string, refreshToken: string) { + let resp; + try { + resp = await got.post(DISCORD_ENDPOINT + Routes.oauth2TokenExchange(), { + form: { + client_id: bot.config.clientId, + client_secret: bot.config.clientSecret, + grant_type: 'refresh_token', + refresh_token: refreshToken, + } satisfies RESTPostOAuth2RefreshTokenURLEncodedData + }).json() as RESTPostOAuth2AccessTokenResult; + } catch(err) { + log.error(err); + return; + } + + const sessionData = { + tokenType: resp.token_type, + accessToken: resp.access_token, + refreshToken: resp.refresh_token, + expiresAt: Date.now() + resp.expires_in * 1000, + }; + + return (await db('sessions') + .where('id', sessionId) + .update(sessionData) + .returning('*'))[0]; +} + +export async function getSession(bot: Client, headers: IncomingHttpHeaders) { + const cookie = headers['cookie']; + if (!cookie) return; + + const sessionStr = await getSessionString(cookie); + + const session = await db('sessions') + .where('id', sessionStr) + .first(); + + if (!session) return; + + if (Date.now() < session.expiresAt) return session; + + const newSession = refreshToken(bot, session.id, session.refreshToken); + return newSession; +} + +export async function setSession(sessionId: string, sessionData: Omit) { + const session = await db('sessions') + .where('id', sessionId) + .first(); + + if (session) { + await db('sessions') + .where('id', sessionId) + .update(sessionData); + } else { + await db('sessions') + .insert({id: sessionId, ...sessionData}) + .returning('*'); + } +} \ No newline at end of file diff --git a/src/web/user.ts b/src/web/user.ts new file mode 100644 index 0000000..9c286ac --- /dev/null +++ b/src/web/user.ts @@ -0,0 +1,32 @@ +import { Session } from '../lib/db'; +import * as log from '../lib/log'; +import got from 'got'; +import { APIPartialGuild, APIUser, Routes } from 'discord.js'; +import { DISCORD_ENDPOINT } from './oauth2'; + +export async function getUser(session: Session | undefined) { + if (!session) return null; + try { + return await got(DISCORD_ENDPOINT + Routes.user(), { + headers: { + authorization: `${session.tokenType} ${session.accessToken}` + } + }).json() as APIUser; + } catch(err) { + log.error(err); + return null; + } +} +export async function getGuilds(session: Session | undefined) { + if (!session) return null; + try { + return await got(DISCORD_ENDPOINT + Routes.userGuilds(), { + headers: { + authorization: `${session.tokenType} ${session.accessToken}` + } + }).json() as APIPartialGuild[]; + } catch(err) { + log.error(err); + return null; + } +} \ No newline at end of file diff --git a/src/web/web.ts b/src/web/web.ts new file mode 100644 index 0000000..ba0ec0e --- /dev/null +++ b/src/web/web.ts @@ -0,0 +1,130 @@ +import express from 'express'; +import { create } from 'express-handlebars'; +import * as log from '../lib/log'; +import { CustomItem, Counter, CustomCraftingRecipe, db } from '../lib/db'; +import { defaultItems } from '../lib/rpg/items'; +import { Client, CDN } from 'discord.js'; +import { getToken, getSessionString, getSession, setSession, updateCookie } from './oauth2'; +import { getUser, getGuilds } from './user'; + +async function getGuildInfo(bot: Client, id: string) { + const guild = await bot.guilds.cache.get(id); + if (!guild) return; + + const items = await db('customItems') + .where('guild', guild.id) + .count({count: '*'}); + + const counters = await db('counters') + .where('guild', guild.id) + .count({count: '*'}); + + const recipes = await db('customCraftingRecipes') + .where('guild', guild.id) + .count({count: '*'}); + + return { + items: items[0].count as number, + counters: counters[0].count as number, + recipes: recipes[0].count as number, + }; +} + +export async function startServer(bot: Client, port: number) { + const app = express(); + const cdn = new CDN(); + + const hbs = create({ + helpers: { + avatar: (id: string, hash: string) => (id && hash) ? cdn.avatar(id, hash, { size: 128 }) : '/assets/avatar.png', + icon: (id: string, hash: string) => (id && hash) ? cdn.icon(id, hash, { size: 128, forceStatic: true }) : '/assets/avatar.png', + } + }); + + app.engine('handlebars', hbs.engine); + app.set('view engine', 'handlebars'); + app.set('views', './views'); + + app.get('/api/items', async (req, res) => { + const guildID = req.query.guild; + + let customItems : Partial[]; + if (guildID) { + customItems = await db('customItems') + .select('emoji', 'name', 'id', 'description') + .where('guild', guildID) + .limit(25); + } else { + customItems = []; + } + + res.json([...defaultItems, ...customItems]); + }); + + app.get('/api/status', async (_, res) => { + res.json({ + guilds: bot.guilds.cache.size, + uptime: bot.uptime + }); + }); + + app.get('/', async (req, res) => { + const code = req.query.code as string; + + if (code) { + try { + const resp = await getToken(bot, code); + if (!resp) return res.status(400).send('Invalid code provided'); + + const sessionId = await getSessionString(decodeURIComponent(req.headers.cookie || '')); + + setSession(sessionId, { + tokenType: resp.token_type, + accessToken: resp.access_token, + refreshToken: resp.refresh_token, + expiresAt: Date.now() + resp.expires_in * 1000, + }); + + updateCookie(res, sessionId); + + return res.redirect('/profile'); + } catch (err) { + log.error(err); + return res.status(500); + } + } + + const session = await getSession(bot, req.headers); + const user = await getUser(session); + + res.render('home', { + signedIn: session !== undefined, + user: user, + layout: false, + }); + }); + + app.get('/profile', async (req, res) => { + const session = await getSession(bot, req.headers); + if (!session) return res.redirect(`https://discord.com/api/oauth2/authorize?client_id=${bot.config.clientId}&redirect_uri=${encodeURIComponent(bot.config.siteURL)}&response_type=code&scope=identify%20guilds`); + + const user = await getUser(session); + if (!user) return; + const guilds = await getGuilds(session); + if (!guilds) return; + + //res.sendFile('profile/index.html', { root: 'static/' }); + res.render('profile', { + user, + guilds: await Promise.all( + guilds.map(async guild => + ({...guild, jillo: await getGuildInfo(bot, guild.id)}) + ) + ), + }); + }); + + app.use(express.static('static/')); + + app.listen(port, () => log.info(`web interface listening on ${port}`)); +} \ No newline at end of file diff --git a/static/assets/avatar.png b/static/assets/avatar.png new file mode 100644 index 0000000..4a04f81 Binary files /dev/null and b/static/assets/avatar.png differ diff --git a/static/assets/jillo.png b/static/assets/jillo.png new file mode 100644 index 0000000..e9e5e1a Binary files /dev/null and b/static/assets/jillo.png differ diff --git a/static/assets/jillo_small.png b/static/assets/jillo_small.png new file mode 100644 index 0000000..5d89a8d Binary files /dev/null and b/static/assets/jillo_small.png differ diff --git a/static/create-recipe/index.html b/static/create-recipe/index.html new file mode 100644 index 0000000..827fdde --- /dev/null +++ b/static/create-recipe/index.html @@ -0,0 +1,58 @@ + + + + + + jillo + + + + + + + + + + + + +
+
+
+
+ jillo +
+ +
+ +

Recipe Creator

+ +
+ loading available items... +
+ +

Drag items from above into the below lists:

+ +

Inputs Ingredients necessary to create the outputs

+
+
+

Requirements Unlike inputs, these are not consumed, but are necessary

+
+
+

Outputs The result of the recipe

+
+
+ +

Drag an item out of the list in order to remove it

+ +

Recipe string

+

+      Copy-paste this into Jillo to create your recipe
+    
+ + \ No newline at end of file diff --git a/static/create-recipe/script.js b/static/create-recipe/script.js new file mode 100644 index 0000000..f705817 --- /dev/null +++ b/static/create-recipe/script.js @@ -0,0 +1,145 @@ +let resolveLoaded; +const loaded = new Promise(resolve => resolveLoaded = resolve); + +function e(unsafeText) { + let div = document.createElement('div'); + div.innerText = unsafeText; + return div.innerHTML; +} + +/** + * @type {import('../../src/lib/rpg/items').Item | null} + */ +let draggedItem = null; +/** + * @type {Record} + */ +let itemLists = {}; + +function listToString(list) { + return list.map(stack => `${stack.item.id},${stack.quantity}`).join(';'); +} +function updateString() { + document.querySelector('#recipe-string').innerText = [ + listToString(itemLists['inputs'] || []), + listToString(itemLists['requirements'] || []), + listToString(itemLists['outputs'] || []), + ].join('|'); +} + +/** + * @param {import('../../src/lib/rpg/items').Item} item + */ +function renderItem(item) { + const i = document.createElement('div'); + i.innerHTML = ` +
+ ${e(item.emoji)} +
+
+
${e(item.name)}
+
${item.description ? e(item.description) : 'No description'}
+
+ `; + i.classList.add('item'); + i.draggable = true; + i.addEventListener('dragstart', event => { + draggedItem = item; + event.target.classList.add('dragging'); + }); + i.addEventListener('dragend', event => { + draggedItem = null; + event.target.classList.remove('dragging'); + }); + return i; +} +function renderItemStack(item, quantity, type) { + const i = document.createElement('div'); + i.innerHTML = ` +
+ ${e(item.emoji)} +
+
+ x${quantity} +
+ `; + i.classList.add('itemstack'); + i.draggable = true; + i.addEventListener('dragstart', event => { + event.target.classList.add('dragging'); + }); + i.addEventListener('dragend', event => { + event.target.classList.remove('dragging'); + itemLists[type] = itemLists[type] || []; + const items = itemLists[type]; + const stackIdx = items.findIndex(n => n.item.id === item.id); + if (stackIdx !== -1) items.splice(stackIdx, 1); + document.querySelector(`.item-list[data-type="${type}"]`).replaceWith(renderItemList(items, type)); + updateString(); + }); + return i; +} +function renderItemList(items, type) { + const i = document.createElement('div'); + i.textContent = ''; + items.forEach(itemStack => { + i.appendChild(renderItemStack(itemStack.item, itemStack.quantity, type)); + }); + i.dataset.type = type; + i.classList.add('item-list'); + + // prevent default to allow drop + i.addEventListener('dragover', (event) => event.preventDefault(), false); + + i.addEventListener('dragenter', event => draggedItem && event.target.classList.add('dropping')); + i.addEventListener('dragleave', event => draggedItem && event.target.classList.remove('dropping')); + + i.addEventListener('drop', (event) => { + event.preventDefault(); + event.target.classList.remove('dropping'); + + if (!draggedItem) return; + + itemLists[type] = itemLists[type] || []; + const items = itemLists[type]; + + const itemStack = items.find(v => v.item.id === draggedItem.id); + + if (!itemStack) { + items.push({ + item: draggedItem, + quantity: 1 + }); + } else { + itemStack.quantity = itemStack.quantity + 1; + } + + updateString(); + + draggedItem = null; + + event.target.replaceWith(renderItemList(items, type)); + }); + + return i; +} + +Promise.all([ + fetch(`/api/items${document.location.search}`) + .then(res => res.json()), + loaded +]).then(([items]) => { + const itemsContainer = document.querySelector('.items'); + itemsContainer.textContent = ''; + items.forEach(item => + itemsContainer.appendChild(renderItem(item)) + ); + document.querySelectorAll('.item-list').forEach(elem => { + const type = elem.dataset.type; + elem.replaceWith(renderItemList([], type)); + }); + + updateString(); +}); + +document.addEventListener('DOMContentLoaded', () => resolveLoaded()); \ No newline at end of file diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..28a8153 Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..83c38e8 --- /dev/null +++ b/static/script.js @@ -0,0 +1,29 @@ +let resolveLoaded; +const loaded = new Promise(resolve => resolveLoaded = resolve); + +function formatUptime(s) { + let units = [ + ['d', (s / (1000 * 60 * 60 * 24))], + ['h', (s / (1000 * 60 * 60)) % 24], + ['m', (s / (1000 * 60)) % 60], + ['s', (s / 1000) % 60] + ]; + + return units.filter(([_, t]) => t > 1).map(([sh, t]) => `${Math.ceil(t)}${sh}`).join(' '); +} + +Promise.all([ + fetch('/api/status') + .then(res => res.json()), + loaded +]).then(([status]) => { + document.querySelector('#status').innerHTML = ` +
online · ${status.guilds} guilds · up for ${formatUptime(status.uptime)} + `; + const firstRender = Date.now(); + setInterval(() => { + document.querySelector('#uptime').innerText = formatUptime(status.uptime + Date.now() - firstRender); + }, 1000); +}); + +document.addEventListener('DOMContentLoaded', () => resolveLoaded()); \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..4b140e3 --- /dev/null +++ b/static/style.css @@ -0,0 +1,353 @@ +:root { + --accent-color: #f17d10; +} + +body { + padding: 0px; + margin: 0px; + overflow-x: hidden; + color: var(--text-color); + background-color: var(--background-color); + font-family: 'Balsamiq Sans', sans-serif; + font-weight: 300; + width: 100%; + min-height: 100vh; + text-underline-offset: 3px; + font-size: 16px; + color-scheme: light dark; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + display: flex; + flex-direction: column; + justify-content: flex-start; +} + +:root { + --text-color: #111; + --text-color-light: #444; + --background-color: #fefefd; + --background-color-dark: #fafafb; + --background-color-dark-2: #f8f8f9; +} + +@media (prefers-color-scheme: dark) { + :root { + --text-color: #eee; + --text-color-light: #aaa; + --background-color: #111110; + --background-color-dark: #151514; + --background-color-dark-2: #171718; + } +} + +a { + text-decoration: none; + color: var(--accent-color); +} +a:hover { + text-decoration: underline; +} + +#main { + display: flex; + text-align: center; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100%; + flex: 1 0 auto; +} +#main img { + display: block; + height: 18rem; + width: auto; + animation: 1s popup; + animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); + transition: transform 0.15s, opacity 0.1s; +} +#main > img:active { + transform: scale(0.97); + opacity: 0.9; +} +#main > :not(img) { + animation: 0.8s fadein; +} +#main h1 { + font-size: 4rem; + margin-top: 0.5rem; + margin-bottom: 1rem; +} +#main #status { + color: var(--text-color-light); + font-size: 0.85rem; +} +#status .status { + width: 0.5rem; + height: 0.5rem; + background-color: #00a55e; + display: inline-block; + border-radius: 1rem; +} + +@keyframes fadein { + 0% { opacity: 0; } + 100% { opacity: 1; } +} +@keyframes popup { + 0% { transform: scale(0) rotate(40deg) } + 100% { transform: scale(1) rotate(0deg) } +} + +#login { + display: flex; + flex-direction: row; + width: fit-content; + height: 2rem; + border: 1px solid var(--text-color-light); + border-radius: 2rem; + align-items: center; + padding: 0.25em 0.5em; + margin: 0.5rem; + gap: 0.5em; + cursor: pointer; + transition: 0.1s color, 0.1s background-color; + font-size: 1.2rem; + align-self: flex-end; +} +#login:hover { + border-color: var(--accent-color); + background-color: var(--accent-color); +} +#login .avatar { + display: block; + aspect-ratio: 1 / 1; + border-radius: 2rem; + width: auto; + height: 100%; +} +#login:not(:hover) .username.logged-out { + color: var(--text-color-light); +} + +#content { + max-width: 1000px; + width: 100%; + margin: 0 auto; + margin-bottom: 6rem; +} + +.header { + height: 3rem; + display: flex; + align-items: stretch; + padding: 0rem 1rem; + flex-direction: row; + text-shadow: 2px 2px 2px #000000; + margin-bottom: 2rem; +} +.header .bg { + position: absolute; + left: 0%; + right: 0%; + height: 3rem; + background: linear-gradient(#444, #222); + z-index: -1; + border-bottom: 2px ridge #aaa; +} +.header .left { + font-size: 1.5rem; + flex: 1 1 0px; + display: flex; + align-items: center; +} +.header .left a { + text-decoration: none !important; + color: #fff; +} + +.header .links { + flex: 0 0 auto; + text-align: right; + display: flex; + flex-direction: row; + align-items: center; + gap: 0.25rem; +} +.header .links img { + display: block; + width: auto; + height: 100%; +} + +.items { + max-width: 500px; + padding: 1em; + height: 200px; + overflow: auto; + background: linear-gradient(var(--background-color-dark), var(--background-color-dark-2)); + border-radius: 1em; + display: flex; + flex-direction: column; +} +.item { + display: flex; + flex-direction: row; + height: 3rem; + gap: 0.5rem; + outline: 0px solid rgba(255, 255, 255, 0.0); + transition: outline 0.1s; + padding: 0.5rem; + border-radius: 2rem; + cursor: grab; +} +.item:hover { + outline: 1px solid var(--text-color-light); +} +.item.dragging { + outline: 2px dashed var(--text-color-light); + transition: none; + background-color: var(--background-color); +} +.item .icon { + flex: 0 0 auto; + font-size: 1rem; + line-height: 1; + background-color: rgba(199, 199, 199, 0.25); + border-radius: 4rem; + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 1 / 1; + user-select: none; +} +.item .right { + flex: 1 1 0px; + display: flex; + flex-direction: column; + line-height: 1; +} +.item .right, .item .right > * { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.item .right .description { + font-size: 75%; + color: var(--text-color-light); +} +.item-list { + min-height: 2rem; + display: flex; + flex-direction: row; + flex-wrap: wrap; +} +.item-list.dropping::after { + display: flex; + align-items: center; + justify-content: center; + content: '+'; + height: 1.5rem; + width: 1.5rem; + outline: 2px dashed var(--text-color-light); + border-radius: 2rem; + padding: 0.5rem; + margin: 0.25rem; +} +.itemstack { + display: flex; + align-items: center; + height: 1.5rem; + line-height: 1; + padding: 0.5rem; + margin: 0.25rem; + outline: 1px solid var(--text-color-light); + background-color: var(--background-color-dark); + border-radius: 2rem; + gap: 0.5rem; +} +.itemstack.dragging { + outline: 2px dashed var(--text-color-light); + transition: none; + background-color: var(--background-color); +} + +.subtitle { + color: var(--text-color-light); + font-size: 1rem; + font-weight: normal; + margin-left: 0.5rem; +} +.subtitle::before { + content: 'Β·'; + margin-right: 0.5rem; +} + +pre { + background: linear-gradient(var(--background-color-dark), var(--background-color-dark-2)); + overflow: auto; + word-break: normal; + padding: 0.5rem; +} + +.note { + font-style: italic; + color: var(--text-color-light); +} + +.user { + display: flex; + flex-direction: row; + height: 2rem; + padding: 0.5em; + align-items: center; + width: fit-content; + margin: 0 auto; +} +.user .avatar { + display: block; + aspect-ratio: 1 / 1; + border-radius: 2rem; + width: auto; + height: 100%; + margin-right: 0.5em; +} + +.guilds { + display: flex; + flex-direction: column; + align-items: center; +} +.guild { + order: 0; + display: flex; + width: 600px; + max-width: 100%; + height: 3rem; + padding: 0.5rem; + gap: 0.5rem; + margin: 0.5rem; + background-color: var(--background-color-dark); +} +.guild.unavailable { + order: 1; +} +.guild.unavailable .icon { + filter: grayscale(100%); +} +.guild .icon { + flex: 0 0 auto; + display: block; + aspect-ratio: 1 / 1; + border-radius: 2rem; + width: auto; + height: 100%; +} +.guild .right { + display: flex; + flex-direction: column; +} +.guild .info { + color: var(--text-color-light); +} \ No newline at end of file diff --git a/svelte.config.mjs b/svelte.config.mjs new file mode 100644 index 0000000..3bce8ea --- /dev/null +++ b/svelte.config.mjs @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +}; diff --git a/views/home.handlebars b/views/home.handlebars new file mode 100644 index 0000000..5b1e91c --- /dev/null +++ b/views/home.handlebars @@ -0,0 +1,40 @@ + + + + + + jillo + + + + + + + + + + + + +
+ {{#if signedIn}} +
{{user.global_name}}
+ {{else}} +
log in
+ {{/if}} + +
+
+ +

jillo!

+
+ invite + · + repo +
+
+ ··· +
+
+ + \ No newline at end of file diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars new file mode 100644 index 0000000..bc10d4a --- /dev/null +++ b/views/layouts/main.handlebars @@ -0,0 +1,34 @@ + + + + + + jillo + + + + + + + + + + +
+
+
+
+ jillo +
+ +
+ + {{{body}}} +
+ + \ No newline at end of file diff --git a/views/profile.handlebars b/views/profile.handlebars new file mode 100644 index 0000000..52b8ba8 --- /dev/null +++ b/views/profile.handlebars @@ -0,0 +1,27 @@ +
+ +
Logged in as {{user.global_name}}
+
+ +
+

Guilds

+ {{#each guilds}} + {{#if jillo}} +
+ +
+
{{name}}
+
{{jillo.counters}} counters · {{jillo.items}} items · {{jillo.recipes}} recipes
+
+
+ {{else}} +
+ +
+
{{name}}
+
Jillo is not in this server
+
+
+ {{/if}} + {{/each}} +
\ No newline at end of file diff --git a/vite.config.mjs b/vite.config.mjs new file mode 100644 index 0000000..e3a889f --- /dev/null +++ b/vite.config.mjs @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [svelte()], +});