diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 0715f2c687b..6624aec5d8e 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -16,6 +16,10 @@ runs: node-version-file: '.tool-versions' package-manager-cache: false + - name: Setup Bun + if: runner.os != 'Windows' + uses: oven-sh/setup-bun@v2 + - name: Install Node Gyp Build shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }} run: | @@ -25,6 +29,13 @@ runs: shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }} run: pnpm i --frozen-lockfile + - name: Login to Botpress (optional) + if: ${{ env.BP_TOKEN != '' && env.BP_WORKSPACE_ID != '' }} + shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }} + run: | + pnpm dlx @botpress/cli login -y --api-url "$BP_API_URL" --workspace-id "$BP_WORKSPACE_ID" --token "$BP_TOKEN" + pnpm dlx @botpress/adk-cli login --token "$BP_TOKEN" --api-url "$BP_API_URL" + - name: Cache Turbo uses: actions/cache@v5 with: @@ -39,4 +50,7 @@ runs: shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }} env: BP_VERBOSE: 'true' + BP_API_URL: ${{ env.BP_API_URL }} + BP_WORKSPACE_ID: ${{ env.BP_WORKSPACE_ID }} + BP_TOKEN: ${{ env.BP_TOKEN }} run: pnpm turbo run build ${{ inputs.extra_filters }} diff --git a/.github/workflows/run-checks.yml b/.github/workflows/run-checks.yml index 9c32a9ccb99..72988c577a9 100644 --- a/.github/workflows/run-checks.yml +++ b/.github/workflows/run-checks.yml @@ -16,6 +16,10 @@ jobs: - uses: actions/checkout@v2 - name: Setup uses: ./.github/actions/setup + env: + BP_API_URL: 'https://api.botpress.dev' + BP_WORKSPACE_ID: ${{ secrets.STAGING_E2E_TESTS_WORKSPACE_ID }} + BP_TOKEN: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }} - run: pnpm run check:dep - run: pnpm run check:sherif - run: pnpm run check:oxlint diff --git a/.prettierignore b/.prettierignore index 5a72f7385e9..012d3a2b0d7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -21,6 +21,9 @@ pnpm-lock.yaml gen dist .botpress +.adk +**/.adk +**/.adk/** .botpresshome .botpresshome.* bp_modules diff --git a/bots/quack-norris/.gitignore b/bots/quack-norris/.gitignore new file mode 100644 index 00000000000..5168c005fdf --- /dev/null +++ b/bots/quack-norris/.gitignore @@ -0,0 +1,34 @@ +# Dependencies +node_modules/ +.pnpm-store/ + +# Build outputs +dist/ +.adk/ + +# Environment files +.env +.env.local +.env.production + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ + +# Runtime files +*.pid +*.seed +*.pid.lock + +NARRATIVE_BIBLE.md +agent.json \ No newline at end of file diff --git a/bots/quack-norris/.mcp.json b/bots/quack-norris/.mcp.json new file mode 100644 index 00000000000..29a34ffe3f2 --- /dev/null +++ b/bots/quack-norris/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "adk": { + "command": "adk", + "args": ["mcp"] + } + } +} diff --git a/bots/quack-norris/README.md b/bots/quack-norris/README.md new file mode 100644 index 00000000000..2fd9f38cd09 --- /dev/null +++ b/bots/quack-norris/README.md @@ -0,0 +1,36 @@ +# quack-norris + +A Botpress Agent built with the ADK. + +## Getting Started + +1. Install dependencies: + + ```bash + bun install + ``` + +2. Start development server: + + ```bash + adk dev + ``` + +3. Deploy your agent: + ```bash + adk deploy + ``` + +## Project Structure + +- `src/actions/` - Define callable functions +- `src/workflows/` - Define long-running processes +- `src/conversations/` - Define conversation handlers +- `src/tables/` - Define data storage schemas +- `src/triggers/` - Define event subscriptions +- `src/knowledge/` - Add knowledge base files + +## Learn More + +- [ADK Documentation](https://botpress.com/docs/adk) +- [Botpress Platform](https://botpress.com) diff --git a/bots/quack-norris/agent.config.ts b/bots/quack-norris/agent.config.ts new file mode 100644 index 00000000000..cdcb9bdad92 --- /dev/null +++ b/bots/quack-norris/agent.config.ts @@ -0,0 +1,42 @@ +import { z, defineConfig } from '@botpress/runtime' + +export default defineConfig({ + name: 'quack-norris', + description: 'A Discord RPG battle bot - turn-based combat between players', + + defaultModels: { + zai: 'anthropic:claude-sonnet-4-20250514', + autonomous: 'anthropic:claude-sonnet-4-20250514', + }, + + bot: { + state: z.object({}), + }, + + user: { + state: z.object({ + discordUserId: z.string().optional(), + wins: z.number().default(0), + losses: z.number().default(0), + }), + }, + + conversation: { + tags: { + gameId: { title: 'Game ID' }, + phase: { title: 'Phase' }, + }, + }, + + dependencies: { + integrations: { + discord: { + version: 'shell/discord@0.1.0', + enabled: true, + config: { + botToken: process.env.QUACK_NORRIS_DISCORD_BOT_TOKEN ?? '', + }, + }, + }, + }, +}) diff --git a/bots/quack-norris/bun.lock b/bots/quack-norris/bun.lock new file mode 100644 index 00000000000..427cfd72cbd --- /dev/null +++ b/bots/quack-norris/bun.lock @@ -0,0 +1,349 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "quack-norris", + "dependencies": { + "@botpress/runtime": "^1.13.17", + }, + "devDependencies": { + "typescript": "^5.9.3", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], + + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], + + "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-module-imports": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-syntax-jsx": "^7.28.6", "@babel/types": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], + + "@babel/standalone": ["@babel/standalone@7.29.1", "", {}, "sha512-z42abD0C6fiHfgLyCWw8PYv6FCJ0IGVtSCxXk/NPykWO5LNIEGfdLDJ3HdYqlPcAhwtQ3oKH1PvNj2JGpTxQKg=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@botpress/client": ["@botpress/client@1.36.0", "", { "dependencies": { "axios": "^1.6.1", "axios-retry": "^4.5.0", "browser-or-node": "^2.1.1", "qs": "^6.11.0" } }, "sha512-RT+lIcXmYoTKdOKZqwbZ+pEQh7t+6Ole30EZq8Lvnw55w3wM0XUEVybyV23BByd+2iqW8DQb+BVj9joZsJSGRA=="], + + "@botpress/cognitive": ["@botpress/cognitive@0.3.15", "", { "dependencies": { "exponential-backoff": "^3.1.1", "nanoevents": "^9.1.0" } }, "sha512-i3NB9epo08qhT5VHBDT6GGSDlzKI817vV/yxIlAwWPlc+WA9rtur2sLCkDsHT4nivPzNGcJcAXKnwvR6+tnZLQ=="], + + "@botpress/runtime": ["@botpress/runtime@1.15.4", "", { "dependencies": { "@botpress/client": "^1.35.0", "@botpress/cognitive": "^0.3.14", "@botpress/sdk": "^5.4.3", "@botpress/zai": "^2.6.0", "@bpinternal/const": "^0.4.2", "@bpinternal/thicktoken": "^2.0.0", "@bpinternal/zui": "^1.3.3", "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.2.0", "@opentelemetry/core": "^2.2.0", "@opentelemetry/instrumentation": "^0.208.0", "@opentelemetry/instrumentation-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/sdk-trace-node": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.38.0", "axios": "^1.13.2", "bytes": "^3.1.2", "dedent": "^1.7.1", "fast-safe-stringify": "^2.1.1", "fast-xml-parser": "^5.3.3", "glob": "^11.1.0", "llmz": "^0.0.54", "lodash": "^4.17.21", "ms": "^2.1.3", "object-sizeof": "^2.6.5", "p-limit": "^7.3.0", "pretty-bytes": "^7.0.1", "ulid": "^3.0.2", "undici": "^7.16.0" }, "peerDependencies": { "typescript": ">=4.5.0" } }, "sha512-AU4f4WlfhguzILvxoP/RKNABqqQgQ3lsHgcT+3pow06rczzxZq3b7woRA+iCLZaZQfsTlPlW9aOuAIBATUpA2A=="], + + "@botpress/sdk": ["@botpress/sdk@5.4.4", "", { "dependencies": { "@botpress/client": "1.36.0", "browser-or-node": "^2.1.1", "semver": "^7.3.8" }, "peerDependencies": { "@bpinternal/zui": "^1.3.3", "esbuild": "^0.16.12" }, "optionalPeers": ["esbuild"] }, "sha512-n/HrFjmwoFDRAKGssiWfSVN/BbO57Xu3LGnZ0pRcfDutawi1FIVOjjSQMh6aZocNV0Nv70trPrlz4JN4s1QjVQ=="], + + "@botpress/zai": ["@botpress/zai@2.6.1", "", { "dependencies": { "@botpress/cognitive": "0.3.15", "json5": "^2.2.3", "jsonrepair": "^3.10.0", "lodash-es": "^4.17.21", "p-limit": "^7.2.0" }, "peerDependencies": { "@bpinternal/thicktoken": "^1.0.0", "@bpinternal/zui": "^1.3.3" } }, "sha512-54ZOeJSbd6k//BTo0QPpyFEReu/nfi7vXuS5e2TFaVt4eSxJxm+2plGcSDs2xqmtNKgYf2z/VVmBhQAPtn+wsQ=="], + + "@bpinternal/const": ["@bpinternal/const@0.4.2", "", { "dependencies": { "zod": "^3.24.4" } }, "sha512-01q5QJaBKjr6QMzqGI1dFssYDJfWGb1rlu6F7DUD1cvzaHWrbY01zo6YKvl0OkJ//11Is37h8UrOzJ64p55MNA=="], + + "@bpinternal/thicktoken": ["@bpinternal/thicktoken@2.0.0", "", {}, "sha512-K0HmBei6I9wY/jLq0xTgvPJ2hcdhv5lBMB8Vb00cUXFmMwjapzO53mKdHeQzyMC5E9mlwtFSN7ylcTlQDNf5Jw=="], + + "@bpinternal/zui": ["@bpinternal/zui@1.3.3", "", {}, "sha512-nTpX/jzx/zXavPMuCOUmgHJVlV3O/8QM8B3GPdgCXSQfxO5yYuGR4kF/xg0x32Esm3svzvK0qMAr6s8MKdgNKA=="], + + "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], + + "@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.31.0", "", {}, "sha512-1yrgvXlmXH2oNj3eFTrkwacGJbmM0crwipA3ohCrjv52gBeDaD7PsTvFYinlAnqU8iPME3LGP437yk05a2oejw=="], + + "@jitl/quickjs-singlefile-browser-release-sync": ["@jitl/quickjs-singlefile-browser-release-sync@0.31.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.31.0" } }, "sha512-JctBiLmRpxEp83gJWhDcBuFqm5X7T683OLmncN9g0chAHkC8+y5cJmgknaAk5Rb/ANDR3pXMMnGdnGXDdysfBQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], + + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.6.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg=="], + + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA=="], + + "@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/instrumentation": "0.208.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-rhmK46DRWEbQQB77RxmVXGyjs6783crXCnFjYQj+4tDH/Kpv9Rbg3h2kaNyp5Vz2emF1f9HOQQvZoHzwMWOFZQ=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/resources": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ=="], + + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.0", "@opentelemetry/core": "2.6.0", "@opentelemetry/sdk-trace-base": "2.6.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-YhswtasmsbIGEFvLGvR9p/y3PVRTfFf+mgY8van4Ygpnv4sA3vooAjvh+qAn9PNWxs4/IwGGqiQS0PPsaRJ0vQ=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], + + "axios-retry": ["axios-retry@4.5.0", "", { "dependencies": { "is-retry-allowed": "^2.2.0" }, "peerDependencies": { "axios": "0.x || 1.x" } }, "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], + + "brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "browser-or-node": ["browser-or-node@2.1.1", "", {}, "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001776", "", {}, "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw=="], + + "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], + + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + + "fast-xml-builder": ["fast-xml-builder@1.0.0", "", {}, "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ=="], + + "fast-xml-parser": ["fast-xml-parser@5.4.2", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pw/6pIl4k0CSpElPEJhDppLzaixDEuWui2CUQQBH/ECDf7+y6YwA4Gf7Tyb0Rfe4DIMuZipYj4AEL0nACKglvQ=="], + + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], + + "is-retry-allowed": ["is-retry-allowed@2.2.0", "", {}, "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonrepair": ["jsonrepair@3.13.2", "", { "bin": { "jsonrepair": "bin/cli.js" } }, "sha512-Leuly0nbM4R+S5SVJk3VHfw1oxnlEK9KygdZvfUtEtTawNDyzB4qa1xWTmFt1aeoA7sXZkVTRuIixJ8bAvqVUg=="], + + "llmz": ["llmz@0.0.54", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/generator": "^7.26.3", "@babel/parser": "^7.26.3", "@babel/plugin-transform-react-jsx": "^7.25.9", "@babel/preset-typescript": "^7.26.0", "@babel/standalone": "^7.26.4", "@babel/traverse": "^7.26.4", "@babel/types": "^7.26.3", "@jitl/quickjs-singlefile-browser-release-sync": "^0.31.0", "bytes": "^3.1.2", "exponential-backoff": "^3.1.1", "handlebars": "^4.7.8", "lodash-es": "^4.17.21", "lru-cache": "^11.0.2", "ms": "^2.1.3", "prettier": "^3.4.2", "quickjs-emscripten-core": "^0.31.0", "ulid": "^2.3.0" }, "peerDependencies": { "@botpress/client": "1.35.0", "@botpress/cognitive": "0.3.14", "@bpinternal/thicktoken": "^2.0.0", "@bpinternal/zui": "^1.3.3" }, "optionalPeers": ["@botpress/client"] }, "sha512-Tn3Zi2Ox6lKMBCB4q6GX+BocFGd8Igc6hu1tPk4wGmRN/iDGqJ9ms1ygT7xGpkeDCRvBNcSeAXH7ATfyg9elCg=="], + + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + + "lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="], + + "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoevents": ["nanoevents@9.1.0", "", {}, "sha512-Jd0fILWG44a9luj8v5kED4WI+zfkkgwKyRQKItTtlPfEsh7Lznfi1kr8/iZ+XAIss4Qq5GqRB0qtWbaz9ceO/A=="], + + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-sizeof": ["object-sizeof@2.6.5", "", { "dependencies": { "buffer": "^6.0.3" } }, "sha512-Mu3udRqIsKpneKjIEJ2U/s1KmEgpl+N6cEX1o+dDl2aZ+VW5piHqNgomqAk5YMsDoSkpcA8HnIKx1eqGTKzdfw=="], + + "p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + + "pretty-bytes": ["pretty-bytes@7.1.0", "", {}, "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], + + "quickjs-emscripten-core": ["quickjs-emscripten-core@0.31.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.31.0" } }, "sha512-oQz8p0SiKDBc1TC7ZBK2fr0GoSHZKA0jZIeXxsnCyCs4y32FStzCW4d1h6E1sE0uHDMbGITbk2zhNaytaoJwXQ=="], + + "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], + + "ulid": ["ulid@3.0.2", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-yu26mwteFYzBAot7KVMqFGCVpsF6g8wXfJzQUHvu1no3+rRRSFcSV2nKeYvNPLD2J4b08jYBDhHUjeH0ygIl9w=="], + + "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@opentelemetry/instrumentation-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + + "llmz/ulid": ["ulid@2.4.0", "", { "bin": { "ulid": "bin/cli.js" } }, "sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg=="], + } +} diff --git a/bots/quack-norris/package.json b/bots/quack-norris/package.json new file mode 100644 index 00000000000..b2fb21754e8 --- /dev/null +++ b/bots/quack-norris/package.json @@ -0,0 +1,20 @@ +{ + "name": "quack-norris", + "version": "1.0.0", + "description": "A Botpress Agent built with the ADK", + "type": "module", + "packageManager": "bun@latest", + "scripts": { + "dev": "adk dev", + "build": "node -e \"const cp = require('node:child_process'); if (process.env.CI && (!process.env.BP_TOKEN || !process.env.BP_WORKSPACE_ID)) { console.log('Skipping ADK build in CI without Botpress credentials'); process.exit(0); } const result = cp.spawnSync('adk', ['build'], { stdio: 'inherit', shell: process.platform === 'win32' }); process.exit(result.status ?? 1);\"", + "deploy": "adk deploy" + }, + "dependencies": { + "@botpress/runtime": "^1.16.6" + }, + "devDependencies": { + "@botpress/adk": "^1.16.6", + "@botpress/adk-cli": "^1.16.6", + "typescript": "^5.6.3" + } +} diff --git a/bots/quack-norris/src/actions/getGameStatus.ts b/bots/quack-norris/src/actions/getGameStatus.ts new file mode 100644 index 00000000000..ca1e2441c58 --- /dev/null +++ b/bots/quack-norris/src/actions/getGameStatus.ts @@ -0,0 +1,90 @@ +import { Action, z } from '@botpress/runtime' +import { renderPlayerStatus } from '../lib/narration' +import type { Player } from '../lib/types' + +export const getGameStatus = new Action({ + name: 'getGameStatus', + description: 'Get the current status of a game with full class/energy/status details', + + input: z.object({ + gameId: z.string(), + }), + + output: z.object({ + phase: z.string(), + round: z.number(), + players: z.array( + z.object({ + name: z.string(), + hp: z.number(), + maxHp: z.number(), + energy: z.number(), + maxEnergy: z.number(), + alive: z.boolean(), + duckClass: z.string().optional(), + specialCooldown: z.number(), + itemCount: z.number(), + }) + ), + winnerId: z.string().optional(), + formattedStatus: z.string(), + }), + + async handler({ input }) { + const { GamesTable } = await import('../tables/Games') + const { PlayersTable } = await import('../tables/Players') + + const { rows } = await GamesTable.findRows({ filter: { gameId: input.gameId }, limit: 1 }) + const game = rows[0] + if (!game) { + throw new Error(`Game ${input.gameId} not found`) + } + + // Fetch item counts from player profiles (parallel with partial-failure resilience) + const itemCounts = new Map() + const profileResults = await Promise.allSettled( + (game.players as Player[]).map(async (p) => { + const { rows: profileRows } = await PlayersTable.findRows({ + filter: { discordUserId: p.discordUserId }, + limit: 1, + }) + return { id: p.discordUserId, profile: profileRows[0] } + }) + ) + for (const result of profileResults) { + if (result.status === 'fulfilled') { + const count = + result.value.profile?.inventory?.reduce((sum: number, i: { quantity: number }) => sum + i.quantity, 0) ?? 0 + itemCounts.set(result.value.id, count) + } + } + + const players = game.players.map((p: Player) => ({ + name: p.name, + hp: p.hp, + maxHp: p.maxHp, + energy: p.energy, + maxEnergy: p.maxEnergy, + alive: p.alive, + duckClass: p.duckClass, + specialCooldown: p.specialCooldown, + itemCount: itemCounts.get(p.discordUserId) ?? 0, + })) + + const formattedStatus = game.players + .map((p: Player) => { + const items = itemCounts.get(p.discordUserId) ?? 0 + const itemTag = items > 0 ? ` | Items: ${items}` : '' + return `${p.alive ? '❤️' : '💀'} ${renderPlayerStatus(p)}${itemTag}` + }) + .join('\n\n') + + return { + phase: game.phase, + round: game.round, + players, + winnerId: game.winnerId, + formattedStatus: `**Game Status** — Phase: ${game.phase} | Round: ${game.round}\n\n${formattedStatus}`, + } + }, +}) diff --git a/bots/quack-norris/src/actions/resolveRound.ts b/bots/quack-norris/src/actions/resolveRound.ts new file mode 100644 index 00000000000..a73539fdca2 --- /dev/null +++ b/bots/quack-norris/src/actions/resolveRound.ts @@ -0,0 +1,438 @@ +import { Action, z } from '@botpress/runtime' +import { + getChaosEventByName, + fogMissChance, + isFogOfWar, + isFloorIsLava, + isScrambledOrders, + isTreasureChest, + applyFloorIsLava, + scrambleTargets, + applyTreasureChest, + shouldTriggerChaos, +} from '../lib/chaosEvents' +import { DUCK_CLASSES } from '../lib/classes' +import { + calculateDamage, + deductEnergy, + regenerateEnergy, + applyRest, + hasEnoughEnergy, + executeSpecial, + tickStatusEffects, + decrementCooldowns, + processShieldedAttack, + removeStatusEffect, +} from '../lib/combat' +import { + narrateLightAttack, + narrateHeavyAttack, + narrateCriticalHit, + narrateBlockSuccess, + narrateBlockNoAttack, + narrateBlockPartial, + narrateDodge, + narrateElimination, + narrateRest, + generateCommentary, +} from '../lib/narration' +import type { ActionType, CombatEvent, GameAction, Player } from '../lib/types' + +export const resolveRound = new Action({ + name: 'resolveRound', + description: 'Process all player actions for the current round with full combat engine', + + input: z.object({ + gameId: z.string(), + }), + + output: z.object({ + log: z.array(z.string()), + eliminatedPlayers: z.array(z.string()), + gameOver: z.boolean(), + winnerId: z.string().optional(), + chaosEventName: z.string().optional(), + }), + + async handler({ input }) { + const { GamesTable } = await import('../tables/Games') + const { ActionsTable } = await import('../tables/Actions') + + const { rows } = await GamesTable.findRows({ filter: { gameId: input.gameId }, limit: 1 }) + const game = rows[0] + if (!game) { + throw new Error(`Game ${input.gameId} not found`) + } + + const players: Player[] = [...game.players] + const aliveBefore = players.filter((p) => p.alive) + const events: CombatEvent[] = [] + let totalDamageThisRound = 0 + + // --- Read actions from ActionsTable (filter by round directly) --- + const { rows: actionRows } = await ActionsTable.findRows({ + filter: { gameId: input.gameId, round: game.round }, + }) + const VALID_ACTION_TYPES = new Set(['light', 'heavy', 'block', 'rest', 'special', 'forfeit']) + const roundActions: GameAction[] = actionRows + .filter((r) => VALID_ACTION_TYPES.has(r.actionType)) + .map((r) => ({ + discordUserId: r.discordUserId, + type: r.actionType as ActionType, + targetUserId: r.targetUserId, + })) + + // --- Process forfeits first --- + const forfeitActions = roundActions.filter((a) => a.type === 'forfeit') + for (const fa of forfeitActions) { + const player = players.find((p) => p.discordUserId === fa.discordUserId) + if (player && player.alive) { + player.alive = false + player.hp = 0 + events.push({ + text: `${player.name} raises a white feather and surrenders! They waddle out of the arena with dignity intact... mostly.`, + type: 'elimination', + }) + } + } + + // --- AFK default: assign 'rest' to alive players with no submitted action --- + const submittedIds = new Set(roundActions.map((a) => a.discordUserId)) + const forcedRestIds = new Set() + for (const player of players.filter((p) => p.alive)) { + if (!submittedIds.has(player.discordUserId)) { + roundActions.push({ discordUserId: player.discordUserId, type: 'rest' }) + forcedRestIds.add(player.discordUserId) + } + } + + // --- Step 1: Chaos Event (read from game row, not re-rolled) --- + let chaosEvent = null + let chaosEventName: string | undefined + if (shouldTriggerChaos(game.round) && game.chaosEvent) { + chaosEvent = getChaosEventByName(game.chaosEvent) + chaosEventName = game.chaosEvent + + if ( + chaosEvent && + !isFogOfWar(chaosEvent) && + !isFloorIsLava(chaosEvent) && + !isScrambledOrders(chaosEvent) && + !isTreasureChest(chaosEvent) + ) { + const chaosResults = chaosEvent.apply(players) + events.push(...chaosResults) + } + + if (chaosEvent && isScrambledOrders(chaosEvent)) { + scrambleTargets( + roundActions, + players.filter((p) => p.alive) + ) + events.push({ text: 'All targets have been SCRAMBLED by the Quackverse!', type: 'chaos' }) + } + } + + // --- Step 2: Tick status effects --- + const statusEvents = tickStatusEffects(players) + events.push(...statusEvents) + for (const evt of statusEvents) { + if (evt.type === 'status') { + const dmgMatch = evt.text.match(/(\d+) poison damage/) + if (dmgMatch) { + totalDamageThisRound += parseInt(dmgMatch[1]!, 10) + } + } + } + + // --- Step 2b: Check poison deaths --- + for (const player of players) { + if (player.hp <= 0 && player.alive) { + player.alive = false + events.push({ + text: `${player.name} succumbs to the poison! The toxins claim another victim!`, + type: 'elimination', + }) + } + } + + // --- Step 3: Identify blockers and process energy --- + const blockingPlayerIds = new Set(roundActions.filter((a) => a.type === 'block').map((a) => a.discordUserId)) + const restingPlayers: Player[] = [] + + for (const action of roundActions) { + if (action.type === 'forfeit') { + continue + } + const player = players.find((p) => p.discordUserId === action.discordUserId) + if (!player || !player.alive) { + continue + } + + if (!hasEnoughEnergy(player, action.type)) { + action.type = 'rest' + forcedRestIds.add(player.discordUserId) + } + + if (action.type === 'rest') { + applyRest(player) + restingPlayers.push(player) + events.push({ text: narrateRest(player.name), type: 'rest' }) + // Forced rests (AFK or no energy) don't get vulnerability + if (forcedRestIds.has(player.discordUserId)) { + removeStatusEffect(player, 'resting') + } + } else { + deductEnergy(player, action.type) + } + } + + if (chaosEvent && isFloorIsLava(chaosEvent)) { + const lavaEvents = applyFloorIsLava(restingPlayers) + events.push(...lavaEvents) + } + + // --- Step 4: Process blocks --- + for (const action of roundActions) { + if (action.type !== 'block') { + continue + } + const player = players.find((p) => p.discordUserId === action.discordUserId) + if (!player || !player.alive) { + continue + } + + const heavyAttackers = roundActions.filter((a) => a.type === 'heavy' && a.targetUserId === player.discordUserId) + const lightAttackers = roundActions.filter((a) => a.type === 'light' && a.targetUserId === player.discordUserId) + const aliveHeavyAttackers = heavyAttackers + .map((a) => players.find((p) => p.discordUserId === a.discordUserId)) + .filter((p): p is Player => Boolean(p?.alive)) + const aliveLightAttackers = lightAttackers + .map((a) => players.find((p) => p.discordUserId === a.discordUserId)) + .filter((p): p is Player => Boolean(p?.alive)) + const anyAttackers = aliveHeavyAttackers.length + aliveLightAttackers.length + + if (aliveHeavyAttackers.length > 0) { + for (const attacker of aliveHeavyAttackers) { + events.push({ text: narrateBlockSuccess(attacker.name, player.name), type: 'block' }) + } + } else if (anyAttackers === 0) { + events.push({ text: narrateBlockNoAttack(player.name), type: 'block' }) + } + + // Holy Plumage: heal on ANY blocked attack (heavy or light) + if (player.duckClass === 'sirQuacksALot' && anyAttackers > 0) { + player.hp = Math.min(player.maxHp, player.hp + 8) + events.push({ + text: `${player.name}'s Holy Plumage glows! +8 HP healed from successful block!`, + type: 'status', + }) + } + } + + // --- Step 5: Process special abilities --- + let eliminationCount = 0 + for (const action of roundActions) { + if (action.type !== 'special') { + continue + } + const caster = players.find((p) => p.discordUserId === action.discordUserId) + if (!caster || !caster.alive || !caster.duckClass) { + continue + } + + if (caster.specialCooldown > 0) { + events.push({ + text: `${caster.name} tries to use their special but it's on cooldown! (${caster.specialCooldown} rounds)`, + type: 'special', + }) + continue + } + + const target = action.targetUserId ? players.find((p) => p.discordUserId === action.targetUserId) : undefined + const result = executeSpecial(caster, target, players) + events.push(...result.events) + + if (caster.duckClass === 'mallardNorris') { + if (result.kills.length === 0) { + caster.specialCooldown = DUCK_CLASSES[caster.duckClass].specialCooldown + } + } else { + caster.specialCooldown = DUCK_CLASSES[caster.duckClass].specialCooldown + } + + eliminationCount += result.kills.length + } + + // --- Step 6: Process attacks --- + let firstLightAttacker: Player | undefined + for (const action of roundActions) { + if (action.type !== 'light' && action.type !== 'heavy') { + continue + } + + const attacker = players.find((p) => p.discordUserId === action.discordUserId) + const target = players.find((p) => p.discordUserId === action.targetUserId) + + if (!attacker || !target || !attacker.alive || !target.alive) { + continue + } + + if (action.type === 'light' && !firstLightAttacker) { + firstLightAttacker = attacker + } + + // Fog of War: 10% miss chance + if (chaosEvent && isFogOfWar(chaosEvent) && fogMissChance()) { + events.push({ + text: `${attacker.name} swings blindly in the fog and MISSES ${target.name}!`, + type: 'attack', + }) + continue + } + + const isBlocking = blockingPlayerIds.has(target.discordUserId) + + // Capture pre-attack HP for shield restoration + const preAttackHp = target.hp + const result = calculateDamage(attacker, target, action.type, isBlocking) + + if (result.isDodged) { + events.push({ text: narrateDodge(attacker.name, target.name, action.type), type: 'attack' }) + continue + } + + if (result.isBlocked) { + continue + } + + if (result.damage > 0) { + const shieldResult = processShieldedAttack(attacker, target, result.damage) + if (shieldResult.absorbed) { + // Restore to pre-attack HP (shield absorbed the blow) + if (!shieldResult.reflected) { + target.hp = preAttackHp + } + if (shieldResult.reflected) { + events.push({ + text: `${target.name}'s Mirror Decoy activates! Attack negated and ${shieldResult.reflectDamage} dmg reflected to ${attacker.name}!`, + type: 'special', + }) + } else { + events.push({ + text: `${target.name}'s Divine Shield absorbs the blow! Shield shattered!`, + type: 'special', + }) + } + continue + } + } + + totalDamageThisRound += result.damage + + if (result.isCrit) { + events.push({ text: narrateCriticalHit(attacker.name, target.name, result.damage), type: 'attack' }) + } else if (isBlocking && action.type === 'light') { + events.push({ text: narrateBlockPartial(attacker.name, target.name, result.damage), type: 'attack' }) + } else if (action.type === 'light') { + events.push({ text: narrateLightAttack(attacker.name, target.name, result.damage), type: 'attack' }) + } else { + events.push({ text: narrateHeavyAttack(attacker.name, target.name, result.damage), type: 'attack' }) + } + + if (result.comboTriggered) { + events.push({ + text: `**QUACK COMBO!** ${attacker.name} lands hit #3 in a row! +15 bonus dmg! They're powered up!`, + type: 'attack', + }) + } + + if (result.poisonApplied) { + events.push({ text: `${target.name} has been poisoned by Dr. Quackenstein's Toxic Aura!`, type: 'status' }) + } + } + + if (chaosEvent && isTreasureChest(chaosEvent)) { + const chestEvents = applyTreasureChest(firstLightAttacker) + events.push(...chestEvents) + } + + // --- Step 7: Check eliminations --- + const eliminatedPlayers: string[] = [] + for (const player of players) { + if (player.hp <= 0 && player.alive) { + player.alive = false + eliminatedPlayers.push(player.name) + eliminationCount++ + + const isFirst = eliminationCount === 1 && aliveBefore.length === players.length + const remaining = players.filter((p) => p.alive) + events.push({ text: narrateElimination(player.name, isFirst, remaining), type: 'elimination' }) + } + } + + // --- Step 8: Regenerate energy + decrement cooldowns --- + for (const player of players.filter((p) => p.alive)) { + regenerateEnergy(player) + } + decrementCooldowns(players) + + // --- Step 9: Remove expired resting status --- + for (const player of players) { + if (player.statusEffects) { + player.statusEffects = player.statusEffects.filter((e) => e.type !== 'resting') + } + } + + // --- Step 10: Check game over --- + const alivePlayers = players.filter((p) => p.alive) + const gameOver = alivePlayers.length <= 1 + const winnerId = gameOver && alivePlayers.length === 1 ? alivePlayers[0]!.discordUserId : undefined + + // --- Step 11: Commentary --- + const commentary = generateCommentary(players, eliminatedPlayers.length, totalDamageThisRound) + if (commentary) { + events.push({ text: `\n${commentary}`, type: 'commentary' }) + } + + // --- Step 12: Update game state --- + await GamesTable.upsertRows({ + rows: [ + { + ...game, + players, + actions: [], + round: game.round + 1, + phase: gameOver ? 'finished' : 'combat', + winnerId, + chaosEvent: undefined, + }, + ], + keyColumn: 'gameId', + }) + + // Clean up resolved actions in parallel (avoids sequential N+1 pattern) + await Promise.all(actionRows.map((row) => ActionsTable.deleteRows({ actionKey: row.actionKey }))) + + if (gameOver && winnerId) { + const winner = players.find((p) => p.discordUserId === winnerId) + events.push({ text: `\n${winner?.name ?? 'Unknown'} wins the battle!`, type: 'elimination' }) + } + + let log: string[] + if (chaosEvent && isFogOfWar(chaosEvent)) { + log = [ + '🌫️ **FOG OF WAR** 🌫️', + 'The fog lifts to reveal...', + ...events + .filter((e) => e.type === 'elimination' || e.type === 'chaos' || e.type === 'commentary') + .map((e) => e.text), + `${eliminatedPlayers.length} ducks were eliminated. ${totalDamageThisRound} total damage was dealt. But by whom? Only the fog knows.`, + ] + } else { + log = events.map((e) => e.text) + } + + return { log, eliminatedPlayers, gameOver, winnerId, chaosEventName } + }, +}) diff --git a/bots/quack-norris/src/actions/selectClass.ts b/bots/quack-norris/src/actions/selectClass.ts new file mode 100644 index 00000000000..ec7830a0058 --- /dev/null +++ b/bots/quack-norris/src/actions/selectClass.ts @@ -0,0 +1,81 @@ +import { Action, z } from '@botpress/runtime' +import { DUCK_CLASSES, resolveClassAlias } from '../lib/classes' +import type { Player } from '../lib/types' + +export const selectClass = new Action({ + name: 'selectClass', + description: 'Select a duck class for a player during the class selection phase', + + input: z.object({ + gameId: z.string(), + discordUserId: z.string(), + className: z.string().describe('Class name or alias (e.g. "mallard", "trickster", "paladin", "warlock")'), + }), + + output: z.object({ + success: z.boolean(), + message: z.string(), + className: z.string().optional(), + }), + + async handler({ input }) { + const { GamesTable } = await import('../tables/Games') + + const duckClass = resolveClassAlias(input.className) + if (!duckClass) { + const validNames = Object.values(DUCK_CLASSES) + .map((c) => c.name) + .join(', ') + return { success: false, message: `Unknown class "${input.className}". Valid classes: ${validNames}` } + } + + const { rows } = await GamesTable.findRows({ filter: { gameId: input.gameId }, limit: 1 }) + const game = rows[0] + + if (!game) { + return { success: false, message: `Game ${input.gameId} not found.` } + } + + if (game.phase !== 'classSelection') { + return { success: false, message: 'Class selection is not active right now.' } + } + + const players = [...game.players] + const playerIndex = players.findIndex((p: Player) => p.discordUserId === input.discordUserId) + + if (playerIndex === -1) { + return { success: false, message: 'You are not in this game.' } + } + + const player = players[playerIndex]! + if (player.duckClass) { + const existingClass = DUCK_CLASSES[player.duckClass] + return { success: false, message: `You already picked ${existingClass.name}!` } + } + + const classDef = DUCK_CLASSES[duckClass] + const classTaken = players.some((p: Player) => p.duckClass === duckClass) + if (classTaken) { + return { success: false, message: `**${classDef.name}** is already taken! Pick another class.` } + } + players[playerIndex] = { + ...player, + duckClass, + hp: classDef.maxHp, + maxHp: classDef.maxHp, + energy: classDef.maxEnergy, + maxEnergy: classDef.maxEnergy, + specialCooldown: 0, + statusEffects: [], + consecutiveHits: 0, + } + + await GamesTable.upsertRows({ rows: [{ ...game, players }], keyColumn: 'gameId' }) + + return { + success: true, + message: `${player.name} is now a **${classDef.emoji} ${classDef.name}**! ${classDef.description}`, + className: classDef.name, + } + }, +}) diff --git a/bots/quack-norris/src/actions/startAdventure.ts b/bots/quack-norris/src/actions/startAdventure.ts new file mode 100644 index 00000000000..7a99678d258 --- /dev/null +++ b/bots/quack-norris/src/actions/startAdventure.ts @@ -0,0 +1,120 @@ +import { Action, z } from '@botpress/runtime' +import { TUTORIAL_ENCOUNTER, formatEncounter } from '../lib/encounters' +import { LOCATIONS } from '../lib/locations' +import { PlayersTable } from '../tables/Players' + +export const startAdventure = new Action({ + name: 'startAdventure', + description: 'Create a player profile and return welcome + tutorial text for the guild channel', + + input: z.object({ + discordUserId: z.string().describe('Discord user ID'), + displayName: z.string().describe('Player display name'), + guildId: z.string().optional().describe('Guild ID where !startGame was typed'), + force: z.boolean().default(false).describe('Force-restart adventure (reset adventure state, keep stats)'), + }), + + output: z.object({ + success: z.boolean(), + message: z.string(), + tutorialText: z.string().optional(), + }), + + async handler({ input }) { + // Check if player already exists + const { rows } = await PlayersTable.findRows({ + filter: { discordUserId: input.discordUserId }, + limit: 1, + }) + const existingProfile = rows[0] + + if (existingProfile?.adventureActive && !input.force) { + return { + success: false, + message: + "You're already on an adventure! Use `!startGameForce` to reset, or try `!explore`, `!travel`, `!profile`.", + } + } + + // Create or update player profile — preserve existing stats + const adventureState = input.force + ? { + encounterStep: 0, + encountersCompleted: existingProfile?.adventureState?.encountersCompleted ?? [], + awaitingChoice: 'none' as const, + } + : (existingProfile?.adventureState ?? { encounterStep: 0, encountersCompleted: [], awaitingChoice: 'none' }) + + const baseProfile = { + discordUserId: input.discordUserId, + displayName: input.displayName, + guildId: input.guildId, + adventureActive: true, + totalWins: existingProfile?.totalWins ?? 0, + totalLosses: existingProfile?.totalLosses ?? 0, + totalKills: existingProfile?.totalKills ?? 0, + currentLocation: input.force ? 'coliseum' : (existingProfile?.currentLocation ?? 'coliseum'), + inventory: existingProfile?.inventory ?? [], + adventureState, + level: existingProfile?.level ?? 1, + xp: existingProfile?.xp ?? 0, + breadcrumbs: existingProfile?.breadcrumbs ?? 0, + title: existingProfile?.title ?? 'Fledgling', + titlesUnlocked: existingProfile?.titlesUnlocked ?? ['Fledgling'], + unlockedLocations: existingProfile?.unlockedLocations ?? [ + 'coliseum', + 'puddle', + 'highway', + 'quackatoa', + 'parkBench', + 'frozenPond', + ], + questState: existingProfile?.questState ?? { activeQuests: [], completedQuests: [] }, + } + + // Build welcome text + const startLocation = LOCATIONS.coliseum + const welcome = [ + '**The Great Mallard sneezed, and the universe quacked into existence.**', + '', + 'You open your eyes. Breadcrumb dust swirls in beams of golden light. The roar of ten thousand ducks shakes the stone beneath your webbed feet.', + '', + `You stand at the gates of ${startLocation.emoji} **${startLocation.name}** — the legendary arena where crumbs become legends and legends become crumbs.`, + '', + 'Above, in a nest of pure gold, **Chuck Norris** adjusts his tiny sunglasses and glances your way. He nods once. Just once.', + '', + '*You are now a citizen of The Pond Eternal.*', + '', + '**Commands:**', + '`!explore` — Explore your surroundings', + '`!travel` — Travel to a new location', + '`!inventory` — Check your items', + '`!profile` — View your stats', + '`!help` — See all commands', + ].join('\n') + + // Build tutorial text + const tutorialText = `${formatEncounter(TUTORIAL_ENCOUNTER)}\n\n*Reply with a number to choose.*` + + // Persist profile and tutorial encounter in a single write to avoid partial initialization. + await PlayersTable.upsertRows({ + rows: [ + { + ...baseProfile, + adventureState: { + ...adventureState, + activeEncounterId: TUTORIAL_ENCOUNTER.id, + encounterStep: 1, + }, + }, + ], + keyColumn: 'discordUserId', + }) + + return { + success: true, + message: welcome, + tutorialText, + } + }, +}) diff --git a/bots/quack-norris/src/actions/startGame.ts b/bots/quack-norris/src/actions/startGame.ts new file mode 100644 index 00000000000..2ddd72528d7 --- /dev/null +++ b/bots/quack-norris/src/actions/startGame.ts @@ -0,0 +1,46 @@ +import { Action, z, actions } from '@botpress/runtime' +import { randomUUID } from 'crypto' +import { GamesTable } from '../tables/Games' + +export const startGame = new Action({ + name: 'startGame', + description: 'Create a new RPG game session with a registration poll', + + input: z.object({ + channelId: z.string().describe('Discord channel ID to create the game in'), + }), + + output: z.object({ + gameId: z.string(), + pollMessageId: z.string(), + }), + + async handler({ input }) { + const gameId = `game_${randomUUID()}` + + const { messageId } = await actions.discord.createPoll({ + channelId: input.channelId, + question: 'The Quacktament calls for warriors! Vote to enter the Arena of Mallard Destiny!', + answers: [{ text: 'Enter the Quacktament!' }], + duration: 1, + allowMultiselect: false, + }) + + await GamesTable.upsertRows({ + rows: [ + { + gameId, + channelId: input.channelId, + pollMessageId: messageId, + phase: 'registration', + round: 0, + players: [], + actions: [], + }, + ], + keyColumn: 'gameId', + }) + + return { gameId, pollMessageId: messageId } + }, +}) diff --git a/bots/quack-norris/src/actions/useItem.ts b/bots/quack-norris/src/actions/useItem.ts new file mode 100644 index 00000000000..263250b356a --- /dev/null +++ b/bots/quack-norris/src/actions/useItem.ts @@ -0,0 +1,160 @@ +import { Action, z } from '@botpress/runtime' +import { ITEMS, resolveItemName, type ItemType } from '../lib/items' +import type { Player } from '../lib/types' + +export const useItem = new Action({ + name: 'useItem', + description: 'Use an inventory item during combat', + + input: z.object({ + gameId: z.string().describe('Active game ID'), + discordUserId: z.string().describe('Discord user ID of the player'), + itemName: z.string().describe('Item name or type to use'), + }), + + output: z.object({ + success: z.boolean(), + message: z.string(), + }), + + async handler({ input }) { + const { GamesTable } = await import('../tables/Games') + const { PlayersTable } = await import('../tables/Players') + + // Find the game + const { rows: gameRows } = await GamesTable.findRows({ filter: { gameId: input.gameId }, limit: 1 }) + const game = gameRows[0] + if (!game || game.phase !== 'combat') { + return { success: false, message: 'No active combat to use items in.' } + } + + // Find the player in game + const player = game.players.find((p: Player) => p.discordUserId === input.discordUserId && p.alive) + if (!player) { + return { success: false, message: "You're not in this fight (or you're eliminated)." } + } + + // Find player profile for inventory + const { rows: profileRows } = await PlayersTable.findRows({ + filter: { discordUserId: input.discordUserId }, + limit: 1, + }) + const profile = profileRows[0] + if (!profile) { + return { success: false, message: 'No player profile found. Use `!startGame` first.' } + } + + // Resolve item name to type + const itemType = resolveItemName(input.itemName) + if (!itemType) { + return { success: false, message: `Unknown item "${input.itemName}". Check \`!inventory\` for your items.` } + } + + // Check inventory + const invItem = profile.inventory.find((i: { type: string }) => i.type === itemType) + if (!invItem || invItem.quantity <= 0) { + const def = ITEMS[itemType] + return { success: false, message: `You don't have any ${def.name}!` } + } + + // Apply item effect + const def = ITEMS[itemType] + const players = [...game.players] as Player[] + const gamePlayer = players.find((p) => p.discordUserId === input.discordUserId)! + let effectText = '' + + switch (itemType as ItemType) { + case 'hpPotion': { + const healed = Math.min(20, gamePlayer.maxHp - gamePlayer.hp) + gamePlayer.hp += healed + effectText = `${def.emoji} Used **${def.name}**! Restored ${healed} HP. (${gamePlayer.hp}/${gamePlayer.maxHp})` + break + } + case 'energyDrink': { + const restored = Math.min(15, gamePlayer.maxEnergy - gamePlayer.energy) + gamePlayer.energy += restored + effectText = `${def.emoji} Used **${def.name}**! Restored ${restored} energy. (${gamePlayer.energy}/${gamePlayer.maxEnergy})` + break + } + case 'shieldToken': { + gamePlayer.statusEffects.push({ type: 'shielded', turnsLeft: 2, stacks: 25 }) + effectText = `${def.emoji} Used **${def.name}**! Absorbing up to 25 damage this round.` + break + } + case 'damageBoost': { + gamePlayer.statusEffects.push({ type: 'damageBoost', turnsLeft: 2, stacks: 10 }) + effectText = `${def.emoji} Used **${def.name}**! +10 damage on your next attack.` + break + } + case 'mirrorShard': { + gamePlayer.statusEffects.push({ type: 'decoy', turnsLeft: 2 }) + effectText = `${def.emoji} Used **${def.name}**! Reflecting the next incoming attack back to your attacker!` + break + } + case 'quackGrenade': { + const enemies = players.filter((p) => p.discordUserId !== input.discordUserId && p.alive) + for (const enemy of enemies) { + enemy.hp = Math.max(0, enemy.hp - 15) + if (enemy.hp <= 0) { + enemy.alive = false + } + } + effectText = `${def.emoji} Used **${def.name}**! 💥 15 damage dealt to all enemies!` + break + } + case 'breadcrumbMagnet': { + // Non-combat item — persist boost flag on profile for next encounter + profile.adventureState = { ...profile.adventureState, breadcrumbBoostActive: true } + effectText = `${def.emoji} Used **${def.name}**! Breadcrumb rewards doubled on your next encounter.` + break + } + case 'fogBomb': { + gamePlayer.statusEffects.push({ type: 'dodgeAll', turnsLeft: 2 }) + effectText = `${def.emoji} Used **${def.name}**! 🌫️ A thick fog surrounds you — all attacks miss this round!` + break + } + default: + break + } + + // Apply game effect first — if this fails, item is NOT consumed (safer for player) + const inventory = [...profile.inventory] + const idx = inventory.findIndex((i: { type: string }) => i.type === itemType) + if (idx >= 0) { + inventory[idx] = { ...inventory[idx]!, quantity: inventory[idx]!.quantity - 1 } + if (inventory[idx]!.quantity <= 0) { + inventory.splice(idx, 1) + } + } + + let gameUpdated = false + try { + await GamesTable.upsertRows({ rows: [{ ...game, players }], keyColumn: 'gameId' }) + gameUpdated = true + await PlayersTable.upsertRows({ rows: [{ ...profile, inventory }], keyColumn: 'discordUserId' }) + } catch (e) { + if (gameUpdated) { + try { + // Best-effort rollback to avoid granting a free item effect if inventory write fails. + await GamesTable.upsertRows({ rows: [game], keyColumn: 'gameId' }) + } catch (rollbackError) { + console.error('[useItem] Failed to rollback game state after inventory write failure', { + gameId: input.gameId, + userId: input.discordUserId, + itemType, + rollbackError: rollbackError instanceof Error ? rollbackError.message : String(rollbackError), + }) + } + } + console.error('[useItem] Failed to persist item usage', { + gameId: input.gameId, + userId: input.discordUserId, + itemType, + error: e instanceof Error ? e.message : String(e), + }) + return { success: false, message: '*The item fizzes and sputters.* Something went wrong — try again.' } + } + + return { success: true, message: effectText } + }, +}) diff --git a/bots/quack-norris/src/commands/adventure.ts b/bots/quack-norris/src/commands/adventure.ts new file mode 100644 index 00000000000..19b4c641183 --- /dev/null +++ b/bots/quack-norris/src/commands/adventure.ts @@ -0,0 +1,451 @@ +import type { + CommandContext, + CommandHandler, + ProfileRow, + SendFn, + SetStateFn, + QuestAdvanceFn, +} from '../lib/command-context' +import { rollEncounter, resolveEncounterChoice, getEncounterById, formatEncounter } from '../lib/encounters' +import { ITEMS, addItemToInventory } from '../lib/items' +import { ALL_LOCATION_IDS, formatLocationList, getLocationByIndex, TOLL_RATES, type LocationId } from '../lib/locations' +import { getNpcsAtLocation, formatNpcList } from '../lib/npcs' +import { parseAdventureState } from '../lib/profile' +import { + awardXp, + renderLevelUp, + XP_AWARDS, + BREADCRUMB_AWARDS, + checkTitleUnlocks, + getTitleName, + checkMilestones, +} from '../lib/progression' +import { getQuestById, type QuestProgress } from '../lib/quests' +import { saveProfile } from '../lib/save-profile' + +// --- !startGame --- +const handleStartGame: CommandHandler = async (ctx) => { + const displayName = await ctx.getDisplayName() + const result = await ctx.actions.startAdventure({ + discordUserId: ctx.discordUserId, + displayName, + guildId: ctx.guildId, + force: false, + }) + await ctx.sendText(result.message) + if (result.success && result.tutorialText) { + const p = await ctx.loadProfile() + if (p) { + await ctx.setInteractionState(p, { awaitingChoice: 'encounter' }) + } + await ctx.sendText(result.tutorialText) + } +} + +// --- !startGameForce --- +const handleStartGameForce: CommandHandler = async (ctx) => { + const displayName = await ctx.getDisplayName() + const result = await ctx.actions.startAdventure({ + discordUserId: ctx.discordUserId, + displayName, + guildId: ctx.guildId, + force: true, + }) + await ctx.sendText(result.message) + if (result.success && result.tutorialText) { + const p = await ctx.loadProfile() + if (p) { + await ctx.setInteractionState(p, { awaitingChoice: 'encounter' }) + } + await ctx.sendText(result.tutorialText) + } +} + +// --- Daily encounter refresh (resets non-tutorial encounters every 20 hours) --- +const ENCOUNTER_REFRESH_HOURS = 20 +const maybeRefreshEncounters = async (ctx: CommandContext, profile: ProfileRow): Promise => { + const advState = parseAdventureState(profile.adventureState) + const lastReset = advState.lastEncounterResetAt + if (lastReset) { + const hoursSinceReset = (Date.now() - new Date(lastReset).getTime()) / (1000 * 60 * 60) + if (hoursSinceReset < ENCOUNTER_REFRESH_HOURS) { + return + } + } + // Reset all encounters except tutorial + const kept = (profile.adventureState.encountersCompleted as string[]).filter((id: string) => + id.startsWith('tutorial') + ) + await ctx.withProfile((p) => ({ + ...p, + adventureState: { + ...p.adventureState, + encountersCompleted: kept, + lastEncounterResetAt: new Date().toISOString(), + }, + })) + profile.adventureState = { + ...profile.adventureState, + encountersCompleted: kept, + lastEncounterResetAt: new Date().toISOString(), + } +} + +// --- !explore --- +const handleExplore: CommandHandler = async (ctx) => { + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + + // Refresh encounters daily so locations never permanently run out + await maybeRefreshEncounters(ctx, profile) + + const locationId = profile.currentLocation as LocationId + const qs = profile.questState as { activeQuests?: QuestProgress[] } | undefined + const activeQuestSteps = (qs?.activeQuests ?? []) + .map((q) => { + const def = getQuestById(q.questId) + return def ? { questId: q.questId, stepId: q.currentStepId } : undefined + }) + .filter((s): s is { questId: string; stepId: string } => s !== undefined) + const encounter = rollEncounter(locationId, profile.adventureState.encountersCompleted, activeQuestSteps) + + if (!encounter) { + await ctx.sendText( + '*You search every corner but find nothing new.* This area is fully explored for now.\n\n' + + 'Encounters refresh every **20 hours** — try `!travel` to visit a new location, ' + + 'or come back later for fresh encounters!', + true + ) + return + } + + await ctx.withProfile((p) => ({ + ...p, + adventureState: { + ...p.adventureState, + activeEncounterId: encounter.id, + encounterStep: 1, + lastExploreAt: new Date().toISOString(), + awaitingChoice: 'encounter', + }, + })) + + const formatted = formatEncounter(encounter) + await ctx.sendText(`${formatted}\n\n*Reply with a number to choose.*`, true) +} + +// --- !travel --- +const handleTravel: CommandHandler = async (ctx) => { + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + + const locationId = profile.currentLocation as LocationId + const locationList = formatLocationList(locationId, profile.unlockedLocations) + await ctx.setInteractionState(profile, { awaitingChoice: 'travel' }) + await ctx.sendText(`**Where will you go?**\n\n${locationList}\n\n*Reply with a number to travel.*`, true) +} + +// --- !look --- +const handleLook: CommandHandler = async (ctx) => { + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + const npcsHere = getNpcsAtLocation(profile.currentLocation as LocationId) + if (npcsHere.length === 0) { + await ctx.sendText('Nobody of interest is here right now.', true) + return + } + await ctx.sendText(`**NPCs here:**\n${formatNpcList(npcsHere)}\n\n*Use \`!talk \` to interact.*`, true) +} + +// --- Encounter choice handler --- +export const handleEncounterChoice = async ( + profile: ProfileRow, + choiceNum: number, + send: SendFn, + setState: SetStateFn, + advanceQuests: QuestAdvanceFn +): Promise => { + const encounter = getEncounterById(profile.adventureState.activeEncounterId!) + if (!encounter) { + await setState(profile, { awaitingChoice: 'none' }) + await send( + '*The encounter fades into mist...* Something went wrong. Type `!explore` to find a new adventure.', + true + ) + return + } + + const choice = resolveEncounterChoice(encounter, choiceNum) + if (!choice) { + await send(`Invalid choice. Pick 1-${encounter.choices.length}.`) + return + } + + let resultText = choice.outcome + + // Award item if applicable + const inventory = [...profile.inventory] + if (choice.reward) { + const added = addItemToInventory(inventory, choice.reward) + const itemDef = ITEMS[choice.reward] + if (added) { + resultText += `\n\n**Item acquired:** ${itemDef.emoji} ${itemDef.name} — *${itemDef.effect}*` + resultText += '\n*Use `!use ` during tournament combat to gain an edge!*' + } else { + resultText += `\n\n*Your inventory is full! ${itemDef.emoji} ${itemDef.name} was lost... Use \`!drop \` to make room.*` + } + } + + // Apply breadcrumb risk/reward from encounter choice + const bcDelta = choice.breadcrumbDelta ?? 0 + if (bcDelta < 0) { + const loss = Math.min(Math.abs(bcDelta), profile.breadcrumbs ?? 0) + profile.breadcrumbs = (profile.breadcrumbs ?? 0) - loss + if (loss > 0) { + resultText += `\n\n*Lost ${loss} 🍞 from the ordeal.*` + } + } else if (bcDelta > 0) { + profile.breadcrumbs = (profile.breadcrumbs ?? 0) + bcDelta + resultText += `\n\n*Gained ${bcDelta} bonus 🍞!*` + } + + // Apply XP risk/reward from encounter choice + const xpDelta = choice.xpDelta ?? 0 + if (xpDelta > 0) { + profile.xp = (profile.xp ?? 0) + xpDelta + resultText += `\n*Gained ${xpDelta} bonus XP!*` + } else if (xpDelta < 0) { + const xpLoss = Math.min(Math.abs(xpDelta), profile.xp ?? 0) + profile.xp = Math.max(0, (profile.xp ?? 0) - xpLoss) + if (xpLoss > 0) { + resultText += `\n*Lost ${xpLoss} XP from the setback.*` + } + } + + // Award XP + breadcrumbs for encounter completion (doubled by Breadcrumb Magnet) + const xpGain = XP_AWARDS.encounter + const hasBcBoost = profile.adventureState.breadcrumbBoostActive === true + const bcGain = hasBcBoost ? BREADCRUMB_AWARDS.encounter * 2 : BREADCRUMB_AWARDS.encounter + const xpResult = awardXp(profile.xp ?? 0, xpGain) + profile.xp = xpResult.newXp + profile.breadcrumbs = (profile.breadcrumbs ?? 0) + bcGain + profile.level = xpResult.newLevel + if (hasBcBoost) { + resultText += `\n\n*+${xpGain} XP, +${bcGain} 🍞 (🧲 Breadcrumb Magnet doubled!)*` + } else { + resultText += `\n\n*+${xpGain} XP, +${bcGain} 🍞*` + } + if (xpResult.leveledUp) { + resultText += renderLevelUp(xpResult.oldLevel, xpResult.newLevel, xpResult.newXp) + } + + const completed = [...profile.adventureState.encountersCompleted, encounter.id] + profile.inventory = inventory + profile.adventureState = { + ...profile.adventureState, + activeEncounterId: undefined, + encounterStep: 0, + encountersCompleted: completed, + awaitingChoice: 'none', + pendingQuestId: undefined, + pendingNpcId: undefined, + breadcrumbBoostActive: false, + } + + // Check title unlocks before saving + const newTitles = checkTitleUnlocks(profile as Parameters[0]) + if (newTitles.length > 0) { + profile.titlesUnlocked = [...(profile.titlesUnlocked ?? []), ...newTitles] + for (const t of newTitles) { + resultText += `\n🏆 Title unlocked: **${getTitleName(t)}**` + } + } + + // Check milestone celebrations + const prevMilestone = (profile.adventureState.lastMilestoneIndex as number) ?? -1 + const milestoneResult = checkMilestones(profile as Parameters[0], prevMilestone) + if (milestoneResult.messages.length > 0) { + resultText += '\n\n' + milestoneResult.messages.join('\n') + profile.adventureState = { + ...profile.adventureState, + lastMilestoneIndex: milestoneResult.newIndex, + } + } + + // Single save for all encounter mutations + await saveProfile(profile) + + // Advance quest objectives (uses withProfile for its own atomic read-modify-write) + const locationId = profile.currentLocation as LocationId + const questMsgs = await advanceQuests('completeEncounter', locationId) + if (questMsgs.length > 0) { + resultText += '\n' + questMsgs.join('\n') + } + + // Check for collectItem objectives if item was rewarded + if (choice.reward) { + const itemMsgs = await advanceQuests('collectItem', choice.reward) + if (itemMsgs.length > 0) { + resultText += '\n' + itemMsgs.join('\n') + } + } + + // Post-tutorial onboarding + if (encounter.id === 'tutorial_basics') { + resultText += '\n\n---' + resultText += "\n**Welcome to The Pond Eternal!** Here's what to do next:" + resultText += '\n' + resultText += '\n1. `!explore` — Explore the Coliseum for more encounters and loot' + resultText += "\n2. `!look` — See who's around. Talk to NPCs with `!talk `" + resultText += '\n3. `!talk duchess` — The Duchess has quests for you (once you hit level 2)' + resultText += "\n4. `!travel` — Visit other locations when you're ready" + resultText += '\n5. `!daily` — Pick up a daily quest for bonus rewards' + resultText += '\n' + resultText += '\n*Explore and complete encounters to earn XP and level up. Quests unlock at level 2!*' + } else { + resultText += '\n\n*Type `!explore` to continue exploring or `!travel` to move on.*' + } + await send(resultText, true) +} + +// --- Travel choice handler --- +export const handleTravelChoice = async ( + profile: ProfileRow, + choiceNum: number, + send: SendFn, + setState: SetStateFn, + advanceQuests: QuestAdvanceFn +): Promise => { + const location = getLocationByIndex(choiceNum) + if (!location) { + await send(`Invalid choice. Pick 1-${ALL_LOCATION_IDS.length}.`) + return + } + + if (location.id === profile.currentLocation) { + await send("You're already here! Pick a different destination.") + return + } + + // Check if location is unlocked + const unlocked = profile.unlockedLocations ?? [ + 'coliseum', + 'puddle', + 'highway', + 'quackatoa', + 'parkBench', + 'frozenPond', + ] + if (!unlocked.includes(location.id)) { + let lockMsg = `🔒 **${location.name}** is locked.` + if (location.requiresQuestId) { + const quest = getQuestById(location.requiresQuestId) + lockMsg += ` Complete **${quest?.name ?? location.requiresQuestId}** to unlock.` + } + if (location.requiresLevel) { + lockMsg += ` Requires level ${location.requiresLevel}.` + } + await send(lockMsg, true) + return + } + + // Charge toll for gated locations (first visit after unlocking is free) + const tollDef = TOLL_RATES[location.id] + let tollCharged = false + let tollCost = 0 + if (tollDef) { + const travelAdvState = parseAdventureState(profile.adventureState) + const visitedGated = travelAdvState.visitedGatedLocations + const hasVisitedBefore = visitedGated.includes(location.id) + if (hasVisitedBefore) { + const bc = profile.breadcrumbs ?? 0 + tollCost = tollDef.cost + if (bc < tollCost) { + await send( + `Entry to **${location.name}** costs **${tollCost} 🍞**. You only have ${bc} 🍞 — not enough to enter.`, + true + ) + return + } + profile.breadcrumbs = bc - tollCost + tollCharged = true + } else { + profile.adventureState = { + ...profile.adventureState, + visitedGatedLocations: [...visitedGated, location.id], + } + } + } + + profile.currentLocation = location.id + profile.adventureState = { ...profile.adventureState, awaitingChoice: 'none' } + await saveProfile(profile) + + // Advance quest objectives (uses withProfile for atomic quest updates) + const questMsgs = await advanceQuests('visitLocation', location.id) + let travelText = '' + if (tollCharged && tollCost > 0) { + travelText += `*Entry toll: -${tollCost} 🍞 (${profile.breadcrumbs} 🍞 remaining)*\n\n` + } + travelText += `${location.arrivalText}\n\n*Type \`!explore\` to look around or \`!travel\` to move on.*` + if (questMsgs.length > 0) { + travelText += '\n' + questMsgs.join('\n') + } + + // Show NPCs at this location + const npcsHere = getNpcsAtLocation(location.id) + if (npcsHere.length > 0) { + travelText += `\n\n**NPCs here:** ${npcsHere.map((n) => `${n.emoji} ${n.name}`).join(', ')} — use \`!talk \`` + } + + await send(travelText, true) +} + +// --- !cancel --- +const handleCancel: CommandHandler = async (ctx) => { + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + + const advState = parseAdventureState(profile.adventureState) + if (advState.awaitingChoice === 'none' && !advState.activeEncounterId) { + await ctx.sendText("Nothing to cancel — you're free to roam!", true) + return + } + + await ctx.withProfile((p) => ({ + ...p, + adventureState: { + ...p.adventureState, + awaitingChoice: 'none', + activeEncounterId: undefined, + pendingQuestId: undefined, + pendingNpcId: undefined, + }, + })) + + await ctx.sendText( + '*You step back and clear your head.* Action cancelled. Type `!help` to see what you can do.', + true + ) +} + +export const adventureCommands = new Map([ + ['!startGame', handleStartGame], + ['!startGameForce', handleStartGameForce], + ['!explore', handleExplore], + ['!travel', handleTravel], + ['!look', handleLook], + ['!cancel', handleCancel], +]) diff --git a/bots/quack-norris/src/commands/bounty.ts b/bots/quack-norris/src/commands/bounty.ts new file mode 100644 index 00000000000..7d4952a6368 --- /dev/null +++ b/bots/quack-norris/src/commands/bounty.ts @@ -0,0 +1,123 @@ +import type { CommandHandler } from '../lib/command-context' +import { type LocationId } from '../lib/locations' +import { getNpcsAtLocation } from '../lib/npcs' +import { parseQuestState } from '../lib/profile' +import { generateBountyQuest, getGeneratedQuestsFromProfile, serializeGeneratedQuests } from '../lib/quest-generator' +import { getAvailableQuestsFromNpc } from '../lib/quests' +import { saveProfile } from '../lib/save-profile' + +const BOUNTY_COOLDOWN_MS = 4 * 60 * 60 * 1000 // 4 hours + +const handleBounty: CommandHandler = async (ctx) => { + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + + const qs = parseQuestState(profile.questState) + + // Check: already has an active generated bounty? + const activeBounty = qs.activeQuests.find((q) => q.questId.startsWith('bounty_')) + if (activeBounty) { + const genQuests = getGeneratedQuestsFromProfile(profile.questState) + const def = genQuests.find((g) => g.id === activeBounty.questId) + await ctx.sendText( + `You already have an active bounty: **${def?.name ?? activeBounty.questId}**. Complete or \`!abandon\` it first.`, + true + ) + return + } + + // Check cooldown + const lastCompleted = (qs as Record).lastBountyCompletedAt as string | undefined + if (lastCompleted) { + const elapsed = Date.now() - new Date(lastCompleted).getTime() + if (elapsed < BOUNTY_COOLDOWN_MS) { + const hoursLeft = Math.ceil((BOUNTY_COOLDOWN_MS - elapsed) / (60 * 60 * 1000)) + await ctx.sendText(`The bounty board refreshes in ~${hoursLeft}h. Check back later!`, true) + return + } + } + + // Pick an NPC at current location to be the quest giver + const npcsHere = getNpcsAtLocation(profile.currentLocation as LocationId) + if (npcsHere.length === 0) { + await ctx.sendText('There are no NPCs here to post bounties. Travel somewhere with characters!', true) + return + } + + // Prefer an NPC that has no remaining hardcoded quests + const completedIds = qs.completedQuests.map((q) => q.questId) + const activeIds = qs.activeQuests.map((q) => q.questId) + let giverNpc = npcsHere.find((npc) => { + const available = getAvailableQuestsFromNpc(npc.id, profile.level ?? 1, completedIds, activeIds, qs.completedQuests) + return available.length === 0 + }) + if (!giverNpc) { + giverNpc = npcsHere[Math.floor(Math.random() * npcsHere.length)]! + } + + await ctx.sendText(`*${giverNpc.name} rummages through a stack of papers...* "Hold on, I might have something..."`) + + // Generate the bounty + const genQuests = getGeneratedQuestsFromProfile(profile.questState) + const completedBountyNames = genQuests.filter((g) => completedIds.includes(g.id)).map((g) => g.name) + + let result: Awaited> + try { + result = await generateBountyQuest( + giverNpc.id, + profile.level ?? 1, + profile.currentLocation as LocationId, + completedBountyNames + ) + } catch { + await ctx.sendText( + `*${giverNpc.name} drops the papers.* "Ah... the bounty board is being restocked. Try again in a moment!"`, + true + ) + return + } + + const { generated, definition } = result + + // Store definition and create active quest progress + const updatedGenQuests = [...genQuests, definition] + qs.activeQuests.push({ + questId: definition.id, + currentStepId: definition.steps[0]!.id, + objectiveProgress: {}, + startedAt: new Date().toISOString(), + choicesMade: [], + }) + ;(qs as Record).generatedQuestsJson = serializeGeneratedQuests(updatedGenQuests) + profile.questState = qs as typeof profile.questState + await saveProfile(profile) + ctx.invalidateCache() + + // Display the bounty + const step = definition.steps[0]! + let bountyText = `${definition.emoji} **Bounty: ${definition.name}**\n*${definition.description}*\n\n` + if (generated.flavorText) { + bountyText += `${generated.flavorText}\n\n` + } + bountyText += '**Objectives:**' + for (const obj of step.objectives) { + bountyText += `\n⬜ ${obj.description} (0/${obj.count})` + } + const rewardParts = definition.rewards.map((r) => { + if (r.type === 'xp') { + return `+${r.value} XP` + } + if (r.type === 'breadcrumbs') { + return `+${r.value} 🍞` + } + return String(r.value) + }) + bountyText += `\n\n**Rewards:** ${rewardParts.join(', ')}` + + await ctx.sendText(bountyText, true) +} + +export const bountyCommands = new Map([['!bounty', handleBounty]]) diff --git a/bots/quack-norris/src/commands/combat.ts b/bots/quack-norris/src/commands/combat.ts new file mode 100644 index 00000000000..ec6bdc0a971 --- /dev/null +++ b/bots/quack-norris/src/commands/combat.ts @@ -0,0 +1,491 @@ +import { formatCompactClassList, formatClassDetails, resolveClassAlias, DUCK_CLASSES } from '../lib/classes' +import { hasEnoughEnergy, getSpecialEnergyCost } from '../lib/combat' +import type { CommandHandler, CommandContext } from '../lib/command-context' +import { narrateLoreIntro } from '../lib/narration' +import type { Player } from '../lib/types' +import { GameLoop } from '../workflows/gameLoop' + +const safeReact = async (ctx: CommandContext, emojiId: string): Promise => { + try { + await ctx.actions.discord.addReaction({ + channelId: ctx.channelId, + messageId: ctx.message.tags['discord:id'] ?? ctx.message.id, + emojiId, + }) + } catch { + // Reaction may fail if bot lacks permissions — not critical + } +} + +// --- !startTournament / !game --- +const handleStartTournament: CommandHandler = async (ctx) => { + const targetChannelId = ctx.args[0] ?? ctx.channelId + if (!targetChannelId) { + await ctx.sendText('Could not detect channel. Try `!startTournament `.') + return + } + + const result = await ctx.actions.startGame({ channelId: targetChannelId }) + + ctx.stateRef.activeGameId = result.gameId + ctx.convTags.gameId = result.gameId + ctx.convTags.phase = 'registration' + + await ctx.sendText( + 'The Quacktament has been summoned! A call echoes across The Pond Eternal. Vote on the poll to join the tournament! Type `!start` when all warriors have answered the call.' + ) +} + +// --- !start --- +const handleStart: CommandHandler = async (ctx) => { + if (!ctx.stateRef.activeGameId) { + await ctx.sendText('No tournament is active. Type `!startTournament` to begin one!') + return + } + + const { GamesTable } = await import('../tables/Games') + const { rows } = await GamesTable.findRows({ filter: { gameId: ctx.stateRef.activeGameId }, limit: 1 }) + const game = rows[0] + + if (!game || game.phase !== 'registration') { + await ctx.sendText('*The Arena Scribe checks the roster.* No game is awaiting warriors right now.') + return + } + + if (game.players.length < 2) { + await ctx.sendText( + `*The Arena Scribe counts heads...* Only ${game.players.length} warrior${game.players.length === 1 ? '' : 's'}. The Arena demands at least 2 to begin.` + ) + return + } + + if (game.pollMessageId) { + await ctx.actions.discord.endPoll({ + channelId: game.channelId, + messageId: game.pollMessageId, + }) + } + + await GamesTable.upsertRows({ rows: [{ ...game, phase: 'classSelection' }], keyColumn: 'gameId' }) + ctx.convTags.phase = 'classSelection' + + const loreIntro = narrateLoreIntro() + await ctx.sendText(loreIntro) + + const classList = formatCompactClassList() + await ctx.sendText( + `**Choose your fighter!** Type \`!class \` to pick your duck class:\n\n${classList}\n\n*Type \`!helpclass \` for full details on any class.*\n\nType \`!ready\` when everyone has picked (or they'll be assigned randomly).` + ) +} + +// --- !class --- +const handleClass: CommandHandler = async (ctx) => { + if (!ctx.stateRef.activeGameId) { + await ctx.sendText('No tournament running. Class selection happens during `!startTournament`.') + return + } + + if (!ctx.args[0]) { + await ctx.sendText( + 'Pick a class! Type `!class ` — options: `mallard`, `doc`, `sir`, `trickster`. Use `!helpclass ` for details.' + ) + return + } + + const result = await ctx.actions.selectClass({ + gameId: ctx.stateRef.activeGameId, + discordUserId: ctx.discordUserId, + className: ctx.args[0]!, + }) + + await ctx.sendText(result.message) + + if (result.success) { + await safeReact(ctx, '✅') + } +} + +// --- !helpclass --- +const handleHelpClass: CommandHandler = async (ctx) => { + if (!ctx.args[0]) { + await ctx.sendText('Which class? Type `!helpclass ` — options: `mallard`, `doc`, `sir`, `trickster`.') + return + } + + const duckClass = resolveClassAlias(ctx.args[0]!) + if (!duckClass) { + const validNames = Object.values(DUCK_CLASSES) + .map((c) => c.name) + .join(', ') + await ctx.sendText(`Unknown class "${ctx.args[0]}". Valid classes: ${validNames}`) + return + } + + const details = formatClassDetails(duckClass) + await ctx.sendText(details) +} + +// --- !ready --- +const handleReady: CommandHandler = async (ctx) => { + if (!ctx.stateRef.activeGameId) { + await ctx.sendText('No tournament is active. Type `!startTournament` to begin one!') + return + } + + const { GamesTable } = await import('../tables/Games') + const { rows } = await GamesTable.findRows({ filter: { gameId: ctx.stateRef.activeGameId }, limit: 1 }) + const game = rows[0] + + if (!game || game.phase !== 'classSelection') { + await ctx.sendText("*The Arena Scribe frowns.* Class selection hasn't started yet.") + return + } + + const players = [...game.players] + const allClasses = Object.keys(DUCK_CLASSES) as Array + for (const player of players) { + if (!player.duckClass) { + const randomClass = allClasses[Math.floor(Math.random() * allClasses.length)]! + const classDef = DUCK_CLASSES[randomClass] + player.duckClass = randomClass + player.hp = classDef.maxHp + player.maxHp = classDef.maxHp + player.energy = classDef.maxEnergy + player.maxEnergy = classDef.maxEnergy + player.specialCooldown = 0 + player.statusEffects = [] + player.consecutiveHits = 0 + } + } + + await GamesTable.upsertRows({ + rows: [{ ...game, players, phase: 'combat', round: 1 }], + keyColumn: 'gameId', + }) + ctx.convTags.phase = 'combat' + + await GameLoop.start({ + gameId: game.gameId, + channelId: game.channelId, + conversationId: ctx.conversation.id, + userId: ctx.message.userId, + }) +} + +// --- Combat action helpers --- +const validateCombatAction = async (ctx: CommandContext) => { + if (!ctx.stateRef.activeGameId) { + return null + } + + const { GamesTable } = await import('../tables/Games') + const { ActionsTable } = await import('../tables/Actions') + const { rows } = await GamesTable.findRows({ filter: { gameId: ctx.stateRef.activeGameId }, limit: 1 }) + const game = rows[0] + if (!game || game.phase !== 'combat') { + await ctx.sendText('*The Arena is quiet.* No active combat right now.') + return null + } + + const player = game.players.find((p: Player) => p.discordUserId === ctx.discordUserId && p.alive) + if (!player) { + await ctx.sendText("*The Arena Scribe shakes their head.* You're not in this fight — or you've already fallen.") + return null + } + + const actionKey = `${ctx.stateRef.activeGameId}:${game.round}:${ctx.discordUserId}` + const { rows: existingActions } = await ActionsTable.findRows({ filter: { actionKey }, limit: 1 }) + if (existingActions.length > 0) { + await ctx.sendText("*The Arena Scribe taps their quill.* You've already declared your move this round.") + return null + } + + return { game, player, ActionsTable } +} + +const submitAction = async ( + ActionsTable: { upsertRows: (opts: { rows: unknown[]; keyColumn: string }) => Promise }, + gameId: string, + round: number, + discordUserId: string, + actionType: string, + targetUserId?: string +): Promise => { + const actionKey = `${gameId}:${round}:${discordUserId}` + await ActionsTable.upsertRows({ + rows: [{ actionKey, gameId, round, discordUserId, actionType, targetUserId }], + keyColumn: 'actionKey', + }) +} + +const validateTarget = (game: { players: Player[] }, targetUserId: string, discordUserId: string): string | null => { + if (targetUserId === discordUserId) { + return "*You stare at your own reflection in the arena floor.* You can't target yourself!" + } + const target = game.players.find((p: Player) => p.discordUserId === targetUserId) + if (!target) { + return "*The Arena Scribe scans the roster.* That duck isn't in this fight." + } + if (!target.alive) { + return `*${target.name}\'s feathers lie still on the arena floor.* They\'ve already been eliminated.` + } + return null +} + +// --- !light / !heavy --- +const handleLightHeavy: CommandHandler = async (ctx) => { + if (!ctx.stateRef.activeGameId) { + await ctx.sendText('No tournament is active. Type `!startTournament` to begin one!') + return + } + if (!ctx.args[0]) { + await ctx.sendText(`Who are you attacking? Type \`${ctx.command} @target\`.`) + return + } + + const targetUserId = ctx.args[0]!.replace(/[<@!>]/g, '') + const combat = await validateCombatAction(ctx) + if (!combat) { + return + } + + const targetError = validateTarget(combat.game, targetUserId, ctx.discordUserId) + if (targetError) { + await ctx.sendText(targetError) + return + } + + const actionType = ctx.command === '!light' ? 'light' : 'heavy' + if (!hasEnoughEnergy(combat.player, actionType)) { + await ctx.sendText( + `Not enough energy! You have ${combat.player.energy} energy. ${actionType === 'light' ? 'Light' : 'Heavy'} costs ${actionType === 'light' ? 10 : 25}. Use \`!rest\` to recover.` + ) + return + } + + await submitAction( + combat.ActionsTable, + ctx.stateRef.activeGameId, + combat.game.round, + ctx.discordUserId, + actionType, + targetUserId + ) + + const target = combat.game.players.find((p: Player) => p.discordUserId === targetUserId) + const targetName = target ? target.name : 'their target' + await ctx.sendText( + actionType === 'light' + ? `⚔️ ${combat.player.name} locks eyes on ${targetName}... a swift strike incoming!` + : `🔨 ${combat.player.name} winds up a MASSIVE swing at ${targetName}!` + ) + + await safeReact(ctx, ctx.command === '!light' ? '⚔' : '🔨') +} + +// --- !block --- +const handleBlock: CommandHandler = async (ctx) => { + if (!ctx.stateRef.activeGameId) { + await ctx.sendText('No tournament is active. Type `!startTournament` to begin one!') + return + } + + const combat = await validateCombatAction(ctx) + if (!combat) { + return + } + + if (!hasEnoughEnergy(combat.player, 'block')) { + await ctx.sendText( + `Not enough energy to block! You have ${combat.player.energy} energy. Block costs 10. Use \`!rest\` to recover.` + ) + return + } + + await submitAction(combat.ActionsTable, ctx.stateRef.activeGameId, combat.game.round, ctx.discordUserId, 'block') + await ctx.sendText(`🛡️ ${combat.player.name} hunkers down behind their wings! Blocking!`) + await safeReact(ctx, '🛡') +} + +// --- !special --- +const handleSpecial: CommandHandler = async (ctx) => { + if (!ctx.stateRef.activeGameId) { + await ctx.sendText('No tournament is active. Specials are used during combat.') + return + } + + const combat = await validateCombatAction(ctx) + if (!combat) { + return + } + + if (!combat.player.duckClass) { + await ctx.sendText( + "You don't have a class assigned yet — this shouldn't happen in combat! Try `!status` to check the game." + ) + return + } + + if (combat.player.specialCooldown > 0) { + await ctx.sendText( + `*Your power flickers but won't ignite.* Special on cooldown — ${combat.player.specialCooldown} round${combat.player.specialCooldown !== 1 ? 's' : ''} remaining.` + ) + return + } + + const specialCost = getSpecialEnergyCost(combat.player.duckClass) + if (!hasEnoughEnergy(combat.player, 'special')) { + await ctx.sendText( + `Not enough energy for your special! You have ${combat.player.energy} energy. ${DUCK_CLASSES[combat.player.duckClass].specialName} costs ${specialCost}.` + ) + return + } + + const targetUserId = ctx.args[0] ? ctx.args[0].replace(/[<@!>]/g, '') : undefined + await submitAction( + combat.ActionsTable, + ctx.stateRef.activeGameId, + combat.game.round, + ctx.discordUserId, + 'special', + targetUserId + ) + + const classDef = DUCK_CLASSES[combat.player.duckClass] + await ctx.sendText(`💥 ${combat.player.name} channels their inner power... **${classDef.specialName}** is coming!`) + await safeReact(ctx, '💥') +} + +// --- !rest --- +const handleRest: CommandHandler = async (ctx) => { + if (!ctx.stateRef.activeGameId) { + await ctx.sendText('No tournament is active. Type `!startTournament` to begin one!') + return + } + + const combat = await validateCombatAction(ctx) + if (!combat) { + return + } + + await submitAction(combat.ActionsTable, ctx.stateRef.activeGameId, combat.game.round, ctx.discordUserId, 'rest') + await ctx.sendText( + `💤 ${combat.player.name} sits down and catches their breath... risky, but sometimes you gotta recharge.` + ) + await safeReact(ctx, '💤') +} + +// --- !forfeit --- +const handleForfeit: CommandHandler = async (ctx) => { + if (!ctx.stateRef.activeGameId) { + await ctx.sendText('No tournament is active. Type `!startTournament` to begin one!') + return + } + + const combat = await validateCombatAction(ctx) + if (!combat) { + return + } + + await submitAction(combat.ActionsTable, ctx.stateRef.activeGameId, combat.game.round, ctx.discordUserId, 'forfeit') + await ctx.sendText( + `${combat.player.name} raises a white feather and surrenders! They waddle out of the arena with what remains of their dignity.` + ) + await safeReact(ctx, '🏳️') +} + +// --- !use --- +const handleUse: CommandHandler = async (ctx) => { + if (!ctx.stateRef.activeGameId) { + await ctx.sendText('No tournament is active. Items can be used during combat with `!use `.') + return + } + + if (!ctx.args[0]) { + await ctx.sendText('Which item? Type `!use `. Check `!inventory` to see your items.') + return + } + + // Using an item consumes the round action + const combat = await validateCombatAction(ctx) + if (!combat) { + return + } + + const result = await ctx.actions.useItem({ + gameId: ctx.stateRef.activeGameId, + discordUserId: ctx.discordUserId, + itemName: ctx.args.join(' '), + }) + + if (result.success) { + // Register as a valid round action so the player can't also attack. + // We use 'rest' because it's in ActionsTable's enum and preserves "already acted" semantics. + await submitAction(combat.ActionsTable, ctx.stateRef.activeGameId, combat.game.round, ctx.discordUserId, 'rest') + await safeReact(ctx, '✨') + } + + await ctx.sendText(result.message) +} + +// --- !status --- +const handleStatus: CommandHandler = async (ctx) => { + if (!ctx.stateRef.activeGameId) { + await ctx.sendText('*The Arena is empty.* No tournament is active. Type `!startTournament` to begin one!') + return + } + + const { GamesTable } = await import('../tables/Games') + const { rows } = await GamesTable.findRows({ filter: { gameId: ctx.stateRef.activeGameId }, limit: 1 }) + const game = rows[0] + + if (!game) { + await ctx.sendText("*The Arena Scribe can't find that tournament.* It may have ended.") + return + } + + // Show class selection status during classSelection phase + if (game.phase === 'classSelection') { + const lines = ['*The Arena Scribe checks who has picked their class:*', ''] + for (const p of game.players as Player[]) { + const classDef = p.duckClass ? DUCK_CLASSES[p.duckClass] : undefined + if (classDef) { + lines.push(` ✅ **${p.name}** — ${classDef.emoji} ${classDef.name}`) + } else { + lines.push(` ⏳ **${p.name}** — *still choosing...*`) + } + } + lines.push('') + lines.push("*Type `!ready` when everyone has picked (or they'll be assigned randomly).*") + await ctx.sendText(lines.join('\n')) + return + } + + if (game.phase === 'registration') { + await ctx.sendText( + `*The Arena Scribe counts the roster:* ${game.players.length} warrior${game.players.length !== 1 ? 's' : ''} registered. Type \`!start\` when ready.` + ) + return + } + + const status = await ctx.actions.getGameStatus({ gameId: ctx.stateRef.activeGameId }) + await ctx.sendText(status.formattedStatus) +} + +export const combatCommands = new Map([ + ['!startTournament', handleStartTournament], + ['!game', handleStartTournament], + ['!start', handleStart], + ['!class', handleClass], + ['!helpclass', handleHelpClass], + ['!helpClass', handleHelpClass], + ['!ready', handleReady], + ['!light', handleLightHeavy], + ['!heavy', handleLightHeavy], + ['!block', handleBlock], + ['!special', handleSpecial], + ['!rest', handleRest], + ['!forfeit', handleForfeit], + ['!use', handleUse], + ['!status', handleStatus], +]) diff --git a/bots/quack-norris/src/commands/index.ts b/bots/quack-norris/src/commands/index.ts new file mode 100644 index 00000000000..4411c675d98 --- /dev/null +++ b/bots/quack-norris/src/commands/index.ts @@ -0,0 +1,26 @@ +import type { CommandHandler } from '../lib/command-context' +import { adventureCommands } from './adventure' +import { bountyCommands } from './bounty' +import { combatCommands } from './combat' +import { infoCommands } from './info' +import { questCommands } from './quest' +import { shopCommands } from './shop' + +// Build registry with lowercase keys for case-insensitive lookup +const rawCommands = new Map([ + ...adventureCommands, + ...bountyCommands, + ...questCommands, + ...shopCommands, + ...combatCommands, + ...infoCommands, +]) + +export const commandRegistry = new Map() +for (const [key, handler] of rawCommands) { + commandRegistry.set(key.toLowerCase(), handler) +} + +export { handleEncounterChoice, handleTravelChoice } from './adventure' +export { handleQuestChoice, handleQuestAccept } from './quest' +export { handleShopBuy } from './shop' diff --git a/bots/quack-norris/src/commands/info.ts b/bots/quack-norris/src/commands/info.ts new file mode 100644 index 00000000000..1264325e211 --- /dev/null +++ b/bots/quack-norris/src/commands/info.ts @@ -0,0 +1,260 @@ +import type { CommandHandler } from '../lib/command-context' +import { LOCATIONS, type LocationId } from '../lib/locations' +import { parseQuestState } from '../lib/profile' +import { getLevelForXp, getTitleName, renderXpBar, TITLES } from '../lib/progression' +import { PlayersTable } from '../tables/Players' + +// --- !help --- +const handleHelp: CommandHandler = async (ctx) => { + const helpText = [ + '*Sir David Attenbird adjusts his monocle and unfurls a scroll:*', + '', + "**📖 The Adventurer's Guide to The Pond Eternal**", + '', + '🗺️ **Adventure:**', + '`!startGame` — Begin your adventure in the Quackverse', + '`!explore` — Search your location for encounters & loot', + '`!travel` — Waddle to a new location', + '`!look` — See who lurks nearby', + '`!talk ` — Chat with an NPC (quests, dialogue, wisdom)', + "`!shop` — Browse the local vendor's wares", + '`!buy <#>` — Purchase an item', + '`!inventory` / `!inv` — Rummage through your belongings', + '`!drop ` — Discard an item into the pond', + '`!cancel` — Walk away from a pending choice', + '', + '📜 **Quests & Progression:**', + '`!quests` / `!journal` — Open your quest journal', + '`!daily` — Pick up a daily quest', + '`!abandon ` — Give up on an active quest', + '`!title ` / `!titles` — Flaunt your earned titles', + "`!profile` / `!profile @user` — Consult the Arena Scribe's records", + '', + '🗡️ **Combat:**', + '`!light @target` ⚔️ | `!heavy @target` 🔨 | `!block` 🛡️ | `!special @target` 💥', + '`!rest` 💤 | `!use ` ✨ | `!forfeit` 🏳️', + '', + '📊 **Info:**', + '`!leaderboard` — The sacred scroll of champions', + '', + '*During encounters and quests, reply with a number to choose. Commands are case-insensitive.*', + '', + '*"Remarkable,"* Attenbird whispers. *"The duck actually read the manual."*', + ].join('\n') + + await ctx.sendText(helpText) +} + +// --- !profile --- +const handleProfile: CommandHandler = async (ctx) => { + // !profile @user — View another duck's profile + if (ctx.args[0]) { + const targetId = ctx.args[0].replace(/[<@!>]/g, '') + const { rows } = await PlayersTable.findRows({ filter: { discordUserId: targetId }, limit: 1 }) + const targetProfile = rows[0] + + if (!targetProfile) { + await ctx.sendText( + "*The Arena Scribe flips through dusty pages...* That duck hasn't started their adventure yet!" + ) + return + } + + const location = LOCATIONS[targetProfile.currentLocation as LocationId] + const locationName = location ? `${location.emoji} ${location.name}` : targetProfile.currentLocation + + const tLevel = targetProfile.level ?? 1 + const tXp = targetProfile.xp ?? 0 + const tBc = targetProfile.breadcrumbs ?? 0 + const tTitle = targetProfile.title ?? 'Fledgling' + const tXpInfo = getLevelForXp(tXp) >= 10 ? 'MAX' : renderXpBar(tXp) + + const profileText = [ + '*The Arena Scribe flips through the records:*', + '', + `**${targetProfile.displayName}** — *${getTitleName(tTitle)}*`, + '', + `📊 **Level ${tLevel}** | ${tXpInfo} | 🍞 ${tBc} breadcrumbs`, + `📍 ${locationName}`, + `⚔️ ${targetProfile.totalWins}W / ${targetProfile.totalLosses}L / ${targetProfile.totalKills}K`, + `🗺️ ${targetProfile.adventureState.encountersCompleted.length} encounters completed`, + ].join('\n') + + await ctx.sendText(profileText) + return + } + + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + + const location = LOCATIONS[profile.currentLocation as LocationId] + const locationName = location ? `${location.emoji} ${location.name}` : profile.currentLocation + const level = profile.level ?? 1 + const xp = profile.xp ?? 0 + const bc = profile.breadcrumbs ?? 0 + const title = profile.title ?? 'Fledgling' + const qs = parseQuestState(profile.questState) + const activeCount = qs?.activeQuests.length ?? 0 + const completedCount = qs?.completedQuests.length ?? 0 + const xpInfo = getLevelForXp(xp) >= 10 ? 'MAX' : renderXpBar(xp) + + const profileText = [ + '*The Arena Scribe opens a weathered tome and reads aloud:*', + '', + `**${profile.displayName}** — *${getTitleName(title)}*`, + '', + `📊 **Level ${level}** | ${xpInfo} | 🍞 ${bc} breadcrumbs`, + `📍 ${locationName}`, + `⚔️ ${profile.totalWins}W / ${profile.totalLosses}L / ${profile.totalKills}K`, + `🗺️ ${profile.adventureState.encountersCompleted.length} encounters | ${completedCount} quests done, ${activeCount} active`, + `📦 ${profile.inventory.length}/6 items`, + ].join('\n') + + await ctx.sendText(profileText) +} + +// --- !leaderboard --- +const handleLeaderboard: CommandHandler = async (ctx) => { + if (!ctx.guildId) { + await ctx.sendText('*The Arena Scribe squints...* Could not determine your server. Try again in a guild channel.') + return + } + + const { rows: allPlayers } = await PlayersTable.findRows({ filter: { guildId: ctx.guildId }, limit: 100 }) + if (allPlayers.length === 0) { + await ctx.sendText( + '*The Arena Scribe checks the records...* No adventurers found on this server yet! Type `!startGame` to begin.' + ) + return + } + + const showAdventure = ctx.args[0]?.toLowerCase() === 'adventure' || ctx.args[0]?.toLowerCase() === 'quest' + + if (showAdventure) { + // Adventure leaderboard: sorted by level, then XP, then quests completed + const advSorted = [...allPlayers] + .sort((a, b) => { + const lvlDiff = (b.level ?? 1) - (a.level ?? 1) + if (lvlDiff !== 0) { + return lvlDiff + } + const xpDiff = (b.xp ?? 0) - (a.xp ?? 0) + if (xpDiff !== 0) { + return xpDiff + } + const aQuests = (a.questState as { completedQuests?: unknown[] })?.completedQuests?.length ?? 0 + const bQuests = (b.questState as { completedQuests?: unknown[] })?.completedQuests?.length ?? 0 + return bQuests - aQuests + }) + .slice(0, 10) + const medals = ['🥇', '🥈', '🥉'] + const advLines = advSorted.map((p, i) => { + const medal = medals[i] ?? `**#${i + 1}**` + const title = getTitleName(p.title ?? 'Fledgling') + const questCount = (p.questState as { completedQuests?: unknown[] })?.completedQuests?.length ?? 0 + return `${medal} **${p.displayName}** [${title}] — Lvl ${p.level ?? 1} | ${p.xp ?? 0} XP | ${questCount} quests` + }) + + await ctx.sendText( + '*The Arena Scribe opens a second, dustier scroll:*\n\n' + + `**The Quackverse Explorer Rankings**\n\n${advLines.join('\n')}\n\n` + + `*${advSorted.length} adventurer${advSorted.length !== 1 ? 's' : ''} recorded. Use \`!leaderboard\` for arena rankings.*` + ) + return + } + + // Default: Arena leaderboard + const sorted = [...allPlayers].sort((a, b) => b.totalWins - a.totalWins || b.totalKills - a.totalKills).slice(0, 10) + const medals = ['🥇', '🥈', '🥉'] + const lines = sorted.map((p, i) => { + const medal = medals[i] ?? `**#${i + 1}**` + const title = getTitleName(p.title ?? 'Fledgling') + const kd = `${p.totalWins}W / ${p.totalLosses}L / ${p.totalKills}K` + const lvl = `Lvl ${p.level ?? 1}` + return `${medal} **${p.displayName}** [${title}] — ${kd} | ${lvl}` + }) + + await ctx.sendText( + '*The Arena Scribe unrolls the sacred scroll of champions:*\n\n' + + `**The Arena of Mallard Destiny — Leaderboard**\n\n${lines.join('\n')}\n\n` + + `*${sorted.length} warrior${sorted.length !== 1 ? 's' : ''} recorded. Glory awaits the bold.*\n` + + '*Use `!leaderboard adventure` for explorer rankings.*' + ) +} + +// --- !title / !titles --- +const handleTitle: CommandHandler = async (ctx) => { + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + + // !title — Set display title + if (ctx.args[0]) { + const titleInput = ctx.args.join(' ').toLowerCase() + const matchedTitle = TITLES.find((t) => t.name.toLowerCase() === titleInput || t.id === titleInput) + if (!matchedTitle) { + const available = (profile.titlesUnlocked ?? ['Fledgling']).map((id: string) => getTitleName(id)).join(', ') + await ctx.sendText(`Unknown title. Your unlocked titles: ${available}`) + return + } + + if (!(profile.titlesUnlocked ?? []).includes(matchedTitle.id)) { + await ctx.sendText(`You haven't unlocked **${matchedTitle.name}** yet! ${matchedTitle.condition}.`) + return + } + + await PlayersTable.upsertRows({ + rows: [{ ...profile, title: matchedTitle.id, version: ((profile.version as number) ?? 0) + 1 }], + keyColumn: 'discordUserId', + }) + ctx.invalidateCache() + await ctx.sendText(`Title set to **${matchedTitle.name}**!`, true) + return + } + + // !title (no arg) / !titles — List unlocked titles + const unlocked = (profile.titlesUnlocked ?? ['Fledgling']).map((id: string) => { + const active = id === (profile.title ?? 'Fledgling') ? ' *(active)*' : '' + return `• **${getTitleName(id)}**${active}` + }) + await ctx.sendText(`**Your Titles:**\n${unlocked.join('\n')}\n\n*Use \`!title \` to change.*`) +} + +// --- !channels --- +const handleChannels: CommandHandler = async (ctx) => { + if (!ctx.guildId) { + await ctx.sendText('*Quack.* Could not determine your server.') + return + } + + try { + const result = await ctx.actions.discord.getGuildChannels({ guildId: ctx.guildId }) + const textChannels = (result.channels as { id: string; name?: string; type: number }[]) + .filter((c) => c.type === 0) + .slice(0, 15) + + if (textChannels.length === 0) { + await ctx.sendText('No text channels found. That seems... wrong.') + return + } + + const lines = textChannels.map((c) => `• #${c.name ?? c.id}`) + await ctx.sendText(`**Server Channels:**\n${lines.join('\n')}`) + } catch { + await ctx.sendText('*The Arena Scribe drops their quill.* Could not fetch channels.') + } +} + +export const infoCommands = new Map([ + ['!help', handleHelp], + ['!profile', handleProfile], + ['!leaderboard', handleLeaderboard], + ['!title', handleTitle], + ['!titles', handleTitle], + ['!channels', handleChannels], +]) diff --git a/bots/quack-norris/src/commands/quest.ts b/bots/quack-norris/src/commands/quest.ts new file mode 100644 index 00000000000..3e8719fe1c1 --- /dev/null +++ b/bots/quack-norris/src/commands/quest.ts @@ -0,0 +1,412 @@ +import type { CommandHandler, ProfileRow, SendFn, SetStateFn, CompleteQuestFn } from '../lib/command-context' +import { ITEMS, addItemToInventory } from '../lib/items' +import { LOCATIONS, type LocationId } from '../lib/locations' +import { getNpcsAtLocation, getNpcById, resolveNpcAlias } from '../lib/npcs' +import { parseAdventureState, parseQuestState } from '../lib/profile' +import { awardXp, renderLevelUp, getTitleName } from '../lib/progression' +import { buildQuestLookup, getGeneratedQuestsFromProfile } from '../lib/quest-generator' +import { + ALL_QUESTS, + getQuestById, + getAvailableQuestsFromNpc, + getCurrentStep, + advanceToNextStep, + formatQuestJournal, + type QuestProgress, +} from '../lib/quests' +import { saveProfile } from '../lib/save-profile' +import { PlayersTable } from '../tables/Players' + +// --- !quests / !journal --- +const handleQuests: CommandHandler = async (ctx) => { + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + const qs = parseQuestState(profile.questState) + const genQuests = getGeneratedQuestsFromProfile(profile.questState) + const journal = formatQuestJournal( + qs?.activeQuests ?? [], + qs?.completedQuests ?? [], + profile.level ?? 1, + profile.currentLocation as LocationId, + genQuests + ) + await ctx.sendText(journal, true) +} + +// --- !talk --- +const handleTalk: CommandHandler = async (ctx) => { + if (!ctx.args[0]) { + await ctx.sendText("Who do you want to talk to? Type `!talk ` — try `!look` to see who's around.") + return + } + + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + + const npcId = resolveNpcAlias(ctx.args.join(' ')) + if (!npcId) { + await ctx.sendText(`Unknown NPC "${ctx.args.join(' ')}". Check who's here with \`!look\`.`, true) + return + } + + const npc = getNpcById(npcId) + if (!npc) { + await ctx.sendText('That NPC does not exist.', true) + return + } + + // Check NPC is at current location + const npcsHere = getNpcsAtLocation(profile.currentLocation as LocationId) + if (!npcsHere.some((n) => n.id === npcId)) { + await ctx.sendText( + `${npc.emoji} **${npc.name}** is not at your current location. They're at ${LOCATIONS[npc.location]?.name ?? npc.location}.`, + true + ) + return + } + + // Advance talkToNpc quest objectives + const questMsgs = await ctx.advanceQuestObjectives('talkToNpc', npcId) + + // Check for active quests with this NPC + const qs = parseQuestState(profile.questState) + const activeIds = qs?.activeQuests.map((q) => q.questId) ?? [] + const completedIds = qs?.completedQuests.map((q) => q.questId) ?? [] + + // Check for quests available from this NPC + const available = getAvailableQuestsFromNpc(npcId, profile.level ?? 1, completedIds, activeIds, qs?.completedQuests) + + // Check if the player has completed any quests given by this NPC + const npcQuestIds = ALL_QUESTS.filter((q) => q.giverNpc === npcId).map((q) => q.id) + const hasCompletedNpcQuest = npcQuestIds.some((qId) => completedIds.includes(qId)) + // Chad's postQuestDialogue only triggers if chad_redemption specifically was completed + const usePostQuestDialogue = + npc.dialogue.postQuestDialogue && + hasCompletedNpcQuest && + (npcId !== 'chad' || completedIds.includes('chad_redemption')) + + let responseText = '' + if (questMsgs.length > 0) { + responseText += questMsgs.join('\n') + '\n\n' + } + + if (available.length > 0) { + const quest = available[0]! + // If the player has history with this NPC, greet with postQuestDialogue before the quest offer + if (usePostQuestDialogue) { + responseText += `${npc.dialogue.postQuestDialogue}\n\n` + } + responseText += `${npc.dialogue.questAvailable}\n\n` + responseText += `📜 **${quest.emoji} ${quest.name}** — *${quest.description}*\n\n` + responseText += '**1.** Accept quest\n**2.** Decline' + await ctx.setInteractionState(profile, { + awaitingChoice: 'quest_accept', + pendingQuestId: quest.id, + pendingNpcId: npcId, + }) + } else { + // Check if NPC has quests in progress + const talkGenQuests = getGeneratedQuestsFromProfile(profile.questState) + const talkLookup = buildQuestLookup(talkGenQuests) + const inProgress = qs?.activeQuests.find((q) => { + const qDef = talkLookup(q.questId) + return qDef?.giverNpc === npcId + }) + if (inProgress) { + responseText += npc.dialogue.questInProgress + } else if (usePostQuestDialogue) { + responseText += npc.dialogue.postQuestDialogue + } else if (questMsgs.length === 0) { + responseText += npc.dialogue.greeting + } + } + + await ctx.sendText(responseText, true) +} + +// --- !daily --- +const handleDaily: CommandHandler = async (ctx) => { + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + + const qs = parseQuestState(profile.questState) + + // Check if any daily is already active + const activeDaily = qs?.activeQuests.find((q) => { + const def = getQuestById(q.questId) + return def?.category === 'daily' + }) + if (activeDaily) { + const def = getQuestById(activeDaily.questId) + await ctx.sendText( + `You already have an active daily: **${def?.name ?? activeDaily.questId}**. Complete it first!`, + true + ) + return + } + + // Find available dailies + const dailies = ALL_QUESTS.filter((q) => q.category === 'daily' && q.levelRequired <= (profile.level ?? 1)) + if (dailies.length === 0) { + await ctx.sendText('No daily quests available yet. Keep leveling up!', true) + return + } + + // Pick a random daily that isn't on cooldown + const now = Date.now() + const availableDailies = dailies.filter((d) => { + const completed = qs?.completedQuests + .filter((c) => c.questId === d.id) + .sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime()) + const last = completed?.[0] + if (!last) { + return true + } + const cooldownMs = (d.cooldownHours ?? 20) * 60 * 60 * 1000 + return now - new Date(last.completedAt).getTime() > cooldownMs + }) + + if (availableDailies.length === 0) { + // Find next available cooldown + const nextAvailable = dailies.reduce((earliest: number | null, d) => { + const completed = qs?.completedQuests + .filter((c) => c.questId === d.id) + .sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime()) + const last = completed?.[0] + if (!last) { + return earliest + } + const cooldownMs = (d.cooldownHours ?? 20) * 60 * 60 * 1000 + const readyAt = new Date(last.completedAt).getTime() + cooldownMs + return earliest === null || readyAt < earliest ? readyAt : earliest + }, null) + const timeLeft = nextAvailable ? nextAvailable - now : 0 + const hoursLeft = Math.ceil(timeLeft / (60 * 60 * 1000)) + const timeHint = hoursLeft > 0 ? ` Next daily in ~${hoursLeft}h.` : '' + await ctx.sendText(`You've completed all available dailies recently.${timeHint}`, true) + return + } + + const daily = availableDailies[Math.floor(Math.random() * availableDailies.length)]! + const newQuest: QuestProgress = { + questId: daily.id, + currentStepId: daily.steps[0]!.id, + objectiveProgress: {}, + startedAt: new Date().toISOString(), + choicesMade: [], + } + const questState = qs ?? { activeQuests: [], completedQuests: [] } + questState.activeQuests.push(newQuest) + await PlayersTable.upsertRows({ + rows: [{ ...profile, questState, version: ((profile.version as number) ?? 0) + 1 }], + keyColumn: 'discordUserId', + }) + ctx.invalidateCache() + + const step = daily.steps[0]! + let dailyText = `${daily.emoji} **Daily Quest: ${daily.name}**\n*${daily.description}*\n\n**Objectives:**` + for (const obj of step.objectives) { + dailyText += `\n⬜ ${obj.description} (0/${obj.count})` + } + const rewardParts = daily.rewards.map((r) => { + if (r.type === 'xp') { + return `+${r.value} XP` + } + if (r.type === 'breadcrumbs') { + return `+${r.value} 🍞` + } + return String(r.value) + }) + dailyText += `\n\n**Rewards:** ${rewardParts.join(', ')}` + await ctx.sendText(dailyText, true) +} + +// --- !abandon --- +const handleAbandon: CommandHandler = async (ctx) => { + if (!ctx.args[0]) { + await ctx.sendText('Which quest? Type `!abandon `. Check `!quests` for your active quests.') + return + } + + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + + const questInput = ctx.args.join(' ').toLowerCase() + const qs = parseQuestState(profile.questState) + if (!qs || qs.activeQuests.length === 0) { + await ctx.sendText('You have no active quests to abandon.') + return + } + + const genQuests = getGeneratedQuestsFromProfile(profile.questState) + const lookup = buildQuestLookup(genQuests) + + const match = qs.activeQuests.find((q) => { + const def = lookup(q.questId) + return def?.name.toLowerCase().includes(questInput) || q.questId.includes(questInput) + }) + if (!match) { + await ctx.sendText(`No active quest matching "${ctx.args.join(' ')}". Check \`!quests\`.`) + return + } + + qs.activeQuests = qs.activeQuests.filter((q) => q.questId !== match.questId) + profile.questState = qs as typeof profile.questState + await saveProfile(profile) + + ctx.invalidateCache() + const def = lookup(match.questId) + await ctx.sendText(`Quest **${def?.name ?? match.questId}** abandoned. You can pick it up again later.`, true) +} + +// --- Quest choice handler --- +export const handleQuestChoice = async ( + profile: ProfileRow, + choiceNum: number, + send: SendFn, + setState: SetStateFn, + completeQuestFn: CompleteQuestFn +): Promise => { + const advState = parseAdventureState(profile.adventureState) + const questId = advState.pendingQuestId + if (!questId) { + await setState(profile, { awaitingChoice: 'none' }) + return + } + + const qs = parseQuestState(profile.questState) + const quest = qs?.activeQuests.find((q) => q.questId === questId) + const genQuests = getGeneratedQuestsFromProfile(profile.questState) + const lookup = buildQuestLookup(genQuests) + const def = lookup(questId) + if (!quest || !def) { + await setState(profile, { awaitingChoice: 'none' }) + return + } + + const step = getCurrentStep(quest, def) + if (!step?.choices || choiceNum < 1 || choiceNum > step.choices.length) { + await send(`Invalid choice. Pick 1-${step?.choices?.length ?? 1}.`) + return + } + + const choice = step.choices[choiceNum - 1]! + quest.choicesMade.push(choice.label) + + let responseText = choice.narrative + + // Award choice-specific rewards + if (choice.rewards) { + for (const reward of choice.rewards) { + if (reward.type === 'title' && !profile.titlesUnlocked.includes(reward.value as string)) { + profile.titlesUnlocked = [...profile.titlesUnlocked, reward.value as string] + responseText += `\n🏆 Title unlocked: **${getTitleName(reward.value as string)}**` + } else if (reward.type === 'item') { + const added = addItemToInventory(profile.inventory, reward.value as string) + if (added) { + const itemDef = ITEMS[reward.value as keyof typeof ITEMS] + responseText += `\n📦 Item received: ${itemDef?.emoji ?? '✨'} ${itemDef?.name ?? reward.value}` + } + } else if (reward.type === 'breadcrumbs') { + profile.breadcrumbs = (profile.breadcrumbs ?? 0) + (reward.value as number) + responseText += `\n+${reward.value} 🍞` + } else if (reward.type === 'xp') { + const choiceXpResult = awardXp(profile.xp ?? 0, reward.value as number) + profile.xp = choiceXpResult.newXp + profile.level = choiceXpResult.newLevel + responseText += `\n+${reward.value} XP` + if (choiceXpResult.leveledUp) { + responseText += renderLevelUp(choiceXpResult.oldLevel, choiceXpResult.newLevel, choiceXpResult.newXp) + } + } + } + } + + // Advance to next step + const advance = advanceToNextStep(quest, def, choice.nextStepId) + if (advance.completed) { + const completeMsgs = await completeQuestFn(profile, quest, def) + responseText += '\n' + completeMsgs.join('\n') + } else if (advance.nextStep?.dialogueOnStart) { + responseText += `\n\n📜 ${advance.nextStep.dialogueOnStart}` + } + + profile.adventureState = { + ...profile.adventureState, + awaitingChoice: 'none', + pendingQuestId: undefined, + pendingNpcId: undefined, + } + profile.questState = qs as typeof profile.questState + await saveProfile(profile) + + await send(responseText, true) +} + +// --- Quest accept handler --- +export const handleQuestAccept = async (profile: ProfileRow, send: SendFn, setState: SetStateFn): Promise => { + const advState = parseAdventureState(profile.adventureState) + const questId = advState.pendingQuestId + if (!questId) { + await setState(profile, { awaitingChoice: 'none' }) + return + } + + const def = getQuestById(questId) + if (!def) { + await setState(profile, { awaitingChoice: 'none' }) + return + } + + const newQuest: QuestProgress = { + questId: def.id, + currentStepId: def.steps[0]!.id, + objectiveProgress: {}, + startedAt: new Date().toISOString(), + choicesMade: [], + } + + const qs = parseQuestState(profile.questState) + qs.activeQuests.push(newQuest) + + profile.adventureState = { + ...profile.adventureState, + awaitingChoice: 'none', + pendingQuestId: undefined, + pendingNpcId: undefined, + } + profile.questState = qs as typeof profile.questState + await saveProfile(profile) + + const firstStep = def.steps[0]! + let acceptText = `📜 **Quest accepted: ${def.emoji} ${def.name}**` + if (firstStep.dialogueOnStart) { + acceptText += `\n\n${firstStep.dialogueOnStart}` + } + acceptText += `\n\n**Objective:** ${firstStep.description}` + for (const obj of firstStep.objectives) { + acceptText += `\n⬜ ${obj.description} (0/${obj.count})` + } + + await send(acceptText, true) +} + +export const questCommands = new Map([ + ['!quests', handleQuests], + ['!journal', handleQuests], + ['!talk', handleTalk], + ['!daily', handleDaily], + ['!abandon', handleAbandon], +]) diff --git a/bots/quack-norris/src/commands/shop.ts b/bots/quack-norris/src/commands/shop.ts new file mode 100644 index 00000000000..e45f889aece --- /dev/null +++ b/bots/quack-norris/src/commands/shop.ts @@ -0,0 +1,217 @@ +import type { CommandHandler, ProfileRow, SendFn, SetStateFn, QuestAdvanceFn } from '../lib/command-context' +import { ITEMS, formatInventory, addItemToInventory, resolveItemName } from '../lib/items' +import type { LocationId } from '../lib/locations' +import { getNpcsAtLocation, getNpcById, formatShop } from '../lib/npcs' +import { parseAdventureState, parseQuestState } from '../lib/profile' +import { saveProfile } from '../lib/save-profile' + +// --- !inventory / !inv --- +const handleInventory: CommandHandler = async (ctx) => { + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + + const formatted = formatInventory(profile.inventory) + await ctx.sendText(`**Your Inventory:**\n\n${formatted}`, true) +} + +// --- !drop --- +const handleDrop: CommandHandler = async (ctx) => { + if (!ctx.args[0]) { + await ctx.sendText('Which item? Type `!drop `. Check `!inventory` to see your items.') + return + } + + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + + const itemType = resolveItemName(ctx.args.join(' ')) + if (!itemType) { + await ctx.sendText(`Unknown item "${ctx.args.join(' ')}". Check \`!inventory\` for your items.`) + return + } + + const inventory = [...profile.inventory] + const idx = inventory.findIndex((i: { type: string }) => i.type === itemType) + if (idx < 0 || inventory[idx]!.quantity <= 0) { + const def = ITEMS[itemType] + await ctx.sendText(`You don't have any ${def.name} to drop.`) + return + } + + const def = ITEMS[itemType] + inventory[idx] = { ...inventory[idx]!, quantity: inventory[idx]!.quantity - 1 } + if (inventory[idx]!.quantity <= 0) { + inventory.splice(idx, 1) + } + + profile.inventory = inventory + await saveProfile(profile) + ctx.invalidateCache() + + await ctx.sendText(`${def.emoji} Dropped **${def.name}**. It sinks into the pond. Gone forever.`, true) +} + +// --- !shop --- +const handleShop: CommandHandler = async (ctx) => { + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + + const npcsHere = getNpcsAtLocation(profile.currentLocation as LocationId) + const shopQs = parseQuestState(profile.questState) + const shopCompletedIds = shopQs.completedQuests.map((q) => q.questId) + const shopNpcs = npcsHere.filter((n) => { + if (!n.shopInventory || n.shopInventory.length === 0) { + return false + } + if (n.shopRequiresQuestId && !shopCompletedIds.includes(n.shopRequiresQuestId)) { + return false + } + return true + }) + if (shopNpcs.length === 0) { + // Check if there's a locked shop here + const lockedShop = npcsHere.find((n) => n.shopInventory && n.shopInventory.length > 0 && n.shopRequiresQuestId) + if (lockedShop) { + const questDef = await import('../lib/quests').then((m) => m.getQuestById(lockedShop.shopRequiresQuestId!)) + const questName = questDef?.name ?? lockedShop.shopRequiresQuestId + await ctx.sendText( + `${lockedShop.emoji} **${lockedShop.name}** has a shop, but won't sell to you yet. ` + + `Complete **${questName}** to earn their trust.`, + true + ) + } else { + await ctx.sendText("There's no vendor at this location. Trenchbill hangs out at the Frozen Pond.", true) + } + return + } + + const shopTexts: string[] = [] + for (const sNpc of shopNpcs) { + const sText = formatShop(sNpc, profile.breadcrumbs ?? 0) + if (sText) { + shopTexts.push(sText) + } + } + if (shopTexts.length === 0) { + await ctx.sendText('The shops are empty.', true) + return + } + + await ctx.setInteractionState(profile, { awaitingChoice: 'shop', pendingNpcId: shopNpcs[0]!.id }) + await ctx.sendText(shopTexts.join('\n\n---\n\n'), true) +} + +// --- !buy --- +const handleBuy: CommandHandler = async (ctx) => { + if (!ctx.args[0]) { + await ctx.sendText('Which item? Type `!buy ` after browsing `!shop`.') + return + } + + const num = parseInt(ctx.args[0], 10) + if (isNaN(num)) { + await ctx.sendText("That's not a number! Type `!buy ` after browsing `!shop`.") + return + } + const profile = await ctx.loadProfile() + if (!profile) { + await ctx.sendText("You haven't started your adventure yet! Type `!startGame` first.") + return + } + const npcsHere = getNpcsAtLocation(profile.currentLocation as LocationId) + const buyQs = parseQuestState(profile.questState) + const buyCompletedIds = buyQs.completedQuests.map((q) => q.questId) + const shopNpc = npcsHere.find((n) => { + if (!n.shopInventory || n.shopInventory.length === 0) { + return false + } + if (n.shopRequiresQuestId && !buyCompletedIds.includes(n.shopRequiresQuestId)) { + return false + } + return true + }) + if (!shopNpc?.shopInventory) { + await ctx.sendText("There's no vendor here.", true) + return + } + await ctx.setInteractionState(profile, { pendingNpcId: shopNpc.id }) + await handleShopBuy(profile, num, ctx.sendText, ctx.setInteractionState, ctx.advanceQuestObjectives) +} + +// --- Shop buy handler --- +export const handleShopBuy = async ( + profile: ProfileRow, + itemNum: number, + send: SendFn, + _setState: SetStateFn, + advanceQuests?: QuestAdvanceFn +): Promise => { + const advState = parseAdventureState(profile.adventureState) + const npcId = advState.pendingNpcId + const npc = npcId ? getNpcById(npcId) : undefined + + if (!npc?.shopInventory) { + await send('No shop is active. Browse with `!shop` first.', true) + return + } + if (itemNum < 1 || itemNum > npc.shopInventory.length) { + await send(`Invalid choice. Pick 1-${npc.shopInventory.length}.`) + return + } + + const shopItem = npc.shopInventory[itemNum - 1]! + const bc = profile.breadcrumbs ?? 0 + + if (bc < shopItem.cost) { + await send(`Not enough breadcrumbs! You have ${bc} 🍞, need ${shopItem.cost}.`, true) + return + } + + const qty = shopItem.quantity ?? 1 + for (let i = 0; i < qty; i++) { + const added = addItemToInventory(profile.inventory, shopItem.itemType) + if (!added) { + if (i === 0) { + await send('Your inventory is full! Use `!drop ` to make room.', true) + return + } + break + } + } + + profile.breadcrumbs = bc - shopItem.cost + profile.adventureState = { ...profile.adventureState, awaitingChoice: 'none' } + await saveProfile(profile) + + const itemDef = ITEMS[shopItem.itemType] + const displayName = shopItem.label ?? itemDef.name + const qtyText = qty > 1 ? ` x${qty}` : '' + let purchaseText = `${itemDef.emoji} Purchased **${displayName}**${qtyText} for ${shopItem.cost} 🍞!` + + // Advance spendBreadcrumbs quest objectives + if (advanceQuests) { + const questMsgs = await advanceQuests('spendBreadcrumbs', String(shopItem.cost)) + if (questMsgs.length > 0) { + purchaseText += '\n' + questMsgs.join('\n') + } + } + + await send(purchaseText, true) +} + +export const shopCommands = new Map([ + ['!inventory', handleInventory], + ['!inv', handleInventory], + ['!drop', handleDrop], + ['!shop', handleShop], + ['!buy', handleBuy], +]) diff --git a/bots/quack-norris/src/conversations/discord.ts b/bots/quack-norris/src/conversations/discord.ts new file mode 100644 index 00000000000..da6c2e1e3cb --- /dev/null +++ b/bots/quack-norris/src/conversations/discord.ts @@ -0,0 +1,310 @@ +import { Conversation, z, actions } from '@botpress/runtime' +import { + commandRegistry, + handleEncounterChoice, + handleTravelChoice, + handleQuestChoice, + handleQuestAccept, + handleShopBuy, +} from '../commands' +import type { ProfileRow, InteractionState, CommandContext } from '../lib/command-context' +import { LOCATIONS, type LocationId } from '../lib/locations' +import { parseAdventureState, parseMessagePayload } from '../lib/profile' +import { renderHud, type HudProfile } from '../lib/progression' +import { advanceQuestObjectivesForProfile, completeQuestForProfile } from '../lib/quest-engine' +import { buildQuestLookup, getGeneratedQuestsFromProfile } from '../lib/quest-generator' +import { getQuestById, getQuestStepProgress, type QuestProgress } from '../lib/quests' +import { saveProfile } from '../lib/save-profile' +import { PlayersTable } from '../tables/Players' + +export const Discord = new Conversation({ + channel: 'discord.guildText', + + state: z.object({ + activeGameId: z.string().optional(), + }), + + async handler({ message, conversation, state }) { + if (!message) { + return + } + + const payload = parseMessagePayload((message as unknown as { payload?: unknown }).payload) + const text = (payload.text ?? '').trim() + const [command, ...args] = text.split(/\s+/) + + const discordUserId = message.tags['discord:userId'] ?? message.userId + const channelId = message.tags['discord:channelId'] ?? '' + const convTags = (conversation as unknown as { tags: Record }).tags + const guildId = convTags['discord:guildId'] + const stateRef = state as { activeGameId?: string } + + // --- Profile cache with TTL (5s expiry to avoid stale reads from concurrent commands) --- + const CACHE_TTL_MS = 5_000 + let cachedProfile: ProfileRow | null = null + let cacheTimestamp = 0 + const invalidateCache = (): void => { + cachedProfile = null + cacheTimestamp = 0 + } + + const loadProfile = async (): Promise => { + if (cachedProfile && Date.now() - cacheTimestamp < CACHE_TTL_MS) { + return cachedProfile + } + const { rows } = await PlayersTable.findRows({ filter: { discordUserId }, limit: 1 }) + cachedProfile = rows[0] ?? null + cacheTimestamp = Date.now() + return cachedProfile + } + + const withProfile = async ( + mutate: (profile: ProfileRow) => ProfileRow | Promise + ): Promise => { + const { rows } = await PlayersTable.findRows({ filter: { discordUserId }, limit: 1 }) + const profile = rows[0] as ProfileRow | undefined + if (!profile) { + return null + } + const updated = await mutate(profile) + await saveProfile(updated) + cachedProfile = updated + cacheTimestamp = Date.now() + return updated + } + + // --- Interaction state helpers --- + const getInteractionState = (profile: { adventureState: Record }): InteractionState => { + const parsed = parseAdventureState(profile.adventureState) + return { + awaitingChoice: parsed.awaitingChoice ?? 'none', + pendingQuestId: parsed.pendingQuestId, + pendingNpcId: parsed.pendingNpcId, + } + } + + const setInteractionState = async (profile: ProfileRow, update: Partial): Promise => { + await withProfile((p) => ({ + ...p, + adventureState: { ...p.adventureState, ...update }, + })) + Object.assign(profile.adventureState, update) + } + + // --- Display name helper --- + const getDisplayName = async (): Promise => { + if (guildId) { + try { + const member = await actions.discord.getGuildMember({ guildId, userId: discordUserId }) + const m = member as { nick?: string | null; user?: { username?: string; globalName?: string | null } } + return m.nick ?? m.user?.globalName ?? m.user?.username ?? `Duck_${discordUserId.slice(-4)}` + } catch { + return `Duck_${discordUserId.slice(-4)}` + } + } + return 'Unknown Duck With No Guild' + } + + // --- Send text with optional HUD --- + const sendText = async (t: string, showHud = false): Promise => { + let finalText = t + if (showHud) { + const profile = await loadProfile() + if (profile && profile.level !== undefined) { + const loc = LOCATIONS[profile.currentLocation as LocationId] + const hudProfile: HudProfile = { + displayName: profile.displayName, + title: profile.title ?? 'Fledgling', + level: profile.level ?? 1, + xp: profile.xp ?? 0, + breadcrumbs: profile.breadcrumbs ?? 0, + currentLocation: profile.currentLocation, + inventory: profile.inventory as HudProfile['inventory'], + questState: profile.questState ?? { activeQuests: [] }, + } + const activeQ = hudProfile.questState.activeQuests[0] + let questName: string | undefined + let questProgress: string | undefined + if (activeQ) { + const def = getQuestById(activeQ.questId) + if (def) { + questName = def.name + questProgress = getQuestStepProgress(activeQ as QuestProgress, def) + } + } + finalText += + '\n' + + renderHud(hudProfile, loc?.emoji ?? '📍', loc?.name ?? profile.currentLocation, questName, questProgress) + } + } + await conversation.send({ type: 'text', payload: { text: finalText } }) + } + + // --- Quest objective advancement (delegates to extracted pure logic) --- + const advanceQuestObjectives = async ( + eventType: Parameters[1], + eventTarget?: string + ): Promise => { + const result = { messages: [] as string[] } + + await withProfile((profile) => { + const lookup = buildQuestLookup(getGeneratedQuestsFromProfile(profile.questState)) + const advance = advanceQuestObjectivesForProfile(profile, eventType, eventTarget, lookup) + result.messages = advance.messages + if (advance.interactionUpdate) { + profile.adventureState = { ...profile.adventureState, ...advance.interactionUpdate } + } + return profile + }) + + return result.messages + } + + // --- Quest completion (thin wrapper around extracted pure logic) --- + const completeQuest = async ( + profile: ProfileRow, + quest: QuestProgress, + def: ReturnType & object + ): Promise => { + return completeQuestForProfile(profile, quest, def) + } + + // --- Choice handling (numbered responses for pending states) --- + const choiceNum = parseInt(text, 10) + if (!isNaN(choiceNum) && choiceNum >= 1 && choiceNum <= 8) { + const profile = await loadProfile() + if (profile) { + const iState = getInteractionState(profile) + + if (iState.awaitingChoice === 'encounter' && profile.adventureState.activeEncounterId) { + await handleEncounterChoice(profile, choiceNum, sendText, setInteractionState, advanceQuestObjectives) + return + } + + if (iState.awaitingChoice === 'travel') { + await handleTravelChoice(profile, choiceNum, sendText, setInteractionState, advanceQuestObjectives) + return + } + + if (iState.awaitingChoice === 'quest_choice' && iState.pendingQuestId) { + await handleQuestChoice(profile, choiceNum, sendText, setInteractionState, completeQuest) + return + } + + if (iState.awaitingChoice === 'quest_accept' && iState.pendingQuestId) { + if (choiceNum === 1) { + await handleQuestAccept(profile, sendText, setInteractionState) + return + } else { + await setInteractionState(profile, { + awaitingChoice: 'none', + pendingQuestId: undefined, + }) + await sendText('Maybe another time.', true) + return + } + } + + if (iState.awaitingChoice === 'shop' && iState.pendingNpcId) { + await handleShopBuy(profile, choiceNum, sendText, setInteractionState, advanceQuestObjectives) + return + } + + // Number typed but nothing pending — ignore (normal chatter) + } + return + } + + // --- Pending choice reminder or first-message onboarding for unrecognized input --- + if (command && !command.startsWith('!')) { + const pendProfile = await loadProfile() + if (pendProfile) { + const pendState = getInteractionState(pendProfile) + if (pendState.awaitingChoice !== 'none') { + const reminders: Record = { + encounter: '*The encounter still looms before you.* Pick a number to act, or `!cancel` to flee.', + travel: '*The crossroads await your decision.* Pick a destination number, or `!cancel` to stay put.', + quest_choice: '*A quest decision hangs in the balance.* Choose a number, or `!cancel` to step back.', + quest_accept: '*An NPC watches you expectantly.* **1** to accept, **2** to decline, or `!cancel`.', + shop: '*The vendor taps their foot impatiently.* Pick an item number, or `!cancel` to walk away.', + } + await sendText( + reminders[pendState.awaitingChoice] ?? 'You have a pending choice. Reply with a number or `!cancel`.', + true + ) + return + } + // Existing player typed non-command text with no pending state — ignore silently + // (normal Discord chatter shouldn't trigger bot responses) + } else { + // First-time player onboarding + await sendText( + '*A ripple crosses the pond. Something stirs.*\n\n' + + "Welcome to **The Pond Eternal**, brave duck. You've wandered into a world of quests, combat, and questionable life choices.\n\n" + + 'Type `!startGame` to begin your adventure, or `!help` to see what awaits you.' + ) + return + } + return + } + + // --- Pending-state guard for commands that would start new interactions --- + const interactionCommands = new Set(['!explore', '!travel', '!talk', '!shop', '!daily']) + if (command && interactionCommands.has(command.toLowerCase())) { + const guardProfile = await loadProfile() + if (guardProfile) { + const guardState = getInteractionState(guardProfile) + if (guardState.awaitingChoice !== 'none') { + const guardReminders: Record = { + encounter: '*The encounter demands your attention!* Finish it first (pick a number) or `!cancel` to flee.', + travel: '*The crossroads still await!* Pick a destination or `!cancel` to stay.', + quest_choice: '*A quest decision still hangs in the air.* Choose a number or `!cancel`.', + quest_accept: '*An NPC is still waiting for your answer.* **1** to accept, **2** to decline, or `!cancel`.', + shop: '*The vendor clears their throat.* Pick an item number or `!cancel` to leave.', + } + await sendText( + guardReminders[guardState.awaitingChoice] ?? + '*Something requires your attention.* Finish it first or `!cancel`.', + true + ) + return + } + } + } + + // --- Command dispatch via registry (case-insensitive) --- + const handler = command ? commandRegistry.get(command.toLowerCase()) : undefined + if (handler) { + const ctx: CommandContext = { + command: command!, + discordUserId, + channelId, + guildId, + args, + message: { + id: message.id, + userId: message.userId, + tags: message.tags as Record, + }, + sendText, + loadProfile, + setInteractionState, + getDisplayName, + advanceQuestObjectives, + completeQuest, + withProfile, + invalidateCache, + stateRef, + convTags, + actions, + conversation: { + id: conversation.id, + send: conversation.send.bind(conversation), + }, + } + await handler(ctx) + } else if (command?.startsWith('!')) { + await sendText(`Unknown command \`${command}\`. Type \`!help\` to see available commands.`) + } + }, +}) diff --git a/bots/quack-norris/src/conversations/discordDm.ts b/bots/quack-norris/src/conversations/discordDm.ts new file mode 100644 index 00000000000..bb0ca794678 --- /dev/null +++ b/bots/quack-norris/src/conversations/discordDm.ts @@ -0,0 +1,24 @@ +import { Conversation, z } from '@botpress/runtime' + +export const DiscordDm = new Conversation({ + channel: 'discord.dm', + + state: z.object({}), + + async handler({ message, conversation }) { + if (!message) { + return + } + + await conversation.send({ + type: 'text', + payload: { + text: + '*A duck peers through the darkness of your DMs.*\n\n' + + '**Quack Norris** is a duck-themed RPG played in Discord servers — ' + + 'explore locations, complete quests, fight in tournaments, and uncover the mysteries of The Pond Eternal.\n\n' + + 'All commands work in guild channels. Head to the server and type `!help` to begin your adventure!', + }, + }) + }, +}) diff --git a/bots/quack-norris/src/lib/chaosEvents.ts b/bots/quack-norris/src/lib/chaosEvents.ts new file mode 100644 index 00000000000..67e9f4ce09b --- /dev/null +++ b/bots/quack-norris/src/lib/chaosEvents.ts @@ -0,0 +1,203 @@ +import { addStatusEffect } from './combat' +import type { CombatEvent, Player } from './types' + +export type ChaosEvent = { + name: string + emoji: string + description: string + apply: (players: Player[]) => CombatEvent[] +} + +const pick = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)]! + +const CHAOS_EVENTS: ChaosEvent[] = [ + { + name: 'Quack Storm', + emoji: '🌪️', + description: + 'The sky splits open and a furious vortex of breadcrumbs and feathers descends upon the arena! All fighters are battered for 8 damage as the storm rages!', + apply: (players) => { + const events: CombatEvent[] = [] + for (const p of players.filter((p) => p.alive)) { + p.hp = Math.max(0, p.hp - 8) + events.push({ text: `${p.name} is battered by the Quack Storm! -8 HP!`, type: 'chaos' }) + } + return events + }, + }, + { + name: 'Energy Surge', + emoji: '⚡', + description: + 'The Pond Eternal pulses with primordial energy! The Great Mallard stirs in their cosmic slumber, and raw power floods every fighter with +20% of their max energy!', + apply: (players) => { + const events: CombatEvent[] = [] + for (const p of players.filter((p) => p.alive)) { + const gain = Math.round(p.maxEnergy * 0.2) + p.energy = Math.min(p.maxEnergy, p.energy + gain) + events.push({ text: `${p.name} surges with energy! +${gain} energy!`, type: 'chaos' }) + } + return events + }, + }, + { + name: 'Fog of War', + emoji: '🌫️', + description: + 'A thick, unnatural fog rolls in from the edges of the Quackverse. Sir David Attenbird squints through his monocle but sees nothing. All actions this round are hidden — and attacks have a 10% chance to miss!', + apply: () => { + return [ + { + text: 'The fog descends like a wet blanket over the arena... actions are shrouded in mystery, and attacks may miss!', + type: 'chaos', + }, + ] + }, + }, + { + name: 'Rubber Duck Rain', + emoji: '🦆', + description: + 'The heavens open and a deluge of rubber ducks rains upon the arena! One lucky fighter is bonked on the head by a golden rubber duck and receives +20 HP!', + apply: (players) => { + const alive = players.filter((p) => p.alive) + if (alive.length === 0) { + return [] + } + const lucky = pick(alive) + lucky.hp = Math.min(lucky.maxHp, lucky.hp + 20) + return [ + { + text: `A golden rubber duck bonks ${lucky.name} on the head! +20 HP! The Great Mallard provides!`, + type: 'chaos', + }, + ] + }, + }, + { + name: 'The Floor Is Lava', + emoji: '🌋', + description: + 'Lake Quackatoa has breached the arena floor! Molten breadcrumbs bubble up through the cracks! Any fighter who rests this round will take 15 damage!', + apply: () => { + return [ + { + text: "The arena floor ERUPTS with molten breadcrumbs! Don't stand still! (Resting players take 15 dmg!)", + type: 'chaos', + }, + ] + }, + }, + { + name: 'Chuck Norris Appears', + emoji: '🥋', + description: + 'Chuck Norris descends from his golden nest! He surveys the battlefield, spots the underdog, and places a wing on their shoulder. "Rise." The blessed fighter gains +30% damage for 2 rounds!', + apply: (players) => { + const alive = players.filter((p) => p.alive) + if (alive.length === 0) { + return [] + } + const underdog = alive.reduce((min, p) => (p.hp < min.hp ? p : min), alive[0]!) + addStatusEffect(underdog, { type: 'blessed', turnsLeft: 2 }) + return [ + { + text: `Chuck Norris descends from his golden nest and places a wing on ${underdog.name}'s shoulder. "Rise." 🥋 +30% dmg for 2 rounds!`, + type: 'chaos', + }, + ] + }, + }, + { + name: 'Scrambled Orders', + emoji: '🔀', + description: + 'The Quackverse hiccups! Reality glitches like a bad dream! All attack targets are randomly reassigned — your wing swings where fate decides!', + apply: () => { + return [ + { + text: 'The fabric of reality GLITCHES! All targets this round are SCRAMBLED! Fate chooses your victims!', + type: 'chaos', + }, + ] + }, + }, + { + name: 'Treasure Chest', + emoji: '🎁', + description: + 'A golden treasure chest materializes in the center of the arena, humming with ancient bread magic! The first fighter to use a light attack claims its contents: +15 HP!', + apply: () => { + return [ + { + text: 'A golden treasure chest materializes in the center! First light attacker claims the prize! (+15 HP)', + type: 'chaos', + }, + ] + }, + }, +] + +export const rollChaosEvent = (): ChaosEvent => { + return pick(CHAOS_EVENTS) +} + +export const getChaosEventByName = (name: string): ChaosEvent | undefined => { + return CHAOS_EVENTS.find((e) => e.name === name) +} + +// Random chaos trigger: 20% base + 10% per round after round 2, capped at 80% +export const shouldTriggerChaos = (round: number): boolean => { + if (round < 2) { + return false + } + const probability = Math.min(0.8, 0.2 + (round - 2) * 0.1) + return Math.random() < probability +} + +export const formatChaosAnnouncement = (event: ChaosEvent): string => { + return `\n${event.emoji} **CHAOS EVENT: ${event.name}!** ${event.emoji}\n${event.description}\n` +} + +// Special handling for events that modify round resolution +export const isFogOfWar = (event: ChaosEvent): boolean => event.name === 'Fog of War' +export const isFloorIsLava = (event: ChaosEvent): boolean => event.name === 'The Floor Is Lava' +export const isScrambledOrders = (event: ChaosEvent): boolean => event.name === 'Scrambled Orders' +export const isTreasureChest = (event: ChaosEvent): boolean => event.name === 'Treasure Chest' + +// Fog of War: 10% miss chance on attacks +export const fogMissChance = (): boolean => Math.random() < 0.1 + +export const applyFloorIsLava = (restingPlayers: Player[]): CombatEvent[] => { + const events: CombatEvent[] = [] + for (const p of restingPlayers) { + p.hp = Math.max(0, p.hp - 15) + events.push({ text: `${p.name} rests on LAVA! -15 HP! Bad choice!`, type: 'chaos' }) + } + return events +} + +export const scrambleTargets = ( + playerActions: { discordUserId: string; targetUserId?: string }[], + alivePlayers: Player[] +): void => { + const aliveIds = alivePlayers.map((p) => p.discordUserId) + for (const action of playerActions) { + if (action.targetUserId) { + const otherIds = aliveIds.filter((id) => id !== action.discordUserId) + if (otherIds.length > 0) { + action.targetUserId = pick(otherIds) + } + } + } +} + +export const applyTreasureChest = (firstLightAttacker: Player | undefined): CombatEvent[] => { + if (!firstLightAttacker) { + return [ + { text: 'Nobody used a light attack! The treasure chest vanishes in a puff of breadcrumbs!', type: 'chaos' }, + ] + } + firstLightAttacker.hp = Math.min(firstLightAttacker.maxHp, firstLightAttacker.hp + 15) + return [{ text: `${firstLightAttacker.name} claims the treasure chest! +15 HP! The spoils of speed!`, type: 'chaos' }] +} diff --git a/bots/quack-norris/src/lib/classes.ts b/bots/quack-norris/src/lib/classes.ts new file mode 100644 index 00000000000..f0a5bc4d079 --- /dev/null +++ b/bots/quack-norris/src/lib/classes.ts @@ -0,0 +1,145 @@ +import type { ClassDefinition, DuckClass } from './types' + +export const DUCK_CLASSES: Record = { + mallardNorris: { + id: 'mallardNorris', + name: 'Mallard Norris', + emoji: '🥊', + description: 'A brawler who hits harder the closer to death. Bloodquack passive + Roundhouse Kick special.', + maxHp: 110, + maxEnergy: 100, + attackMod: 0.1, + defenseMod: -0.05, + passiveName: 'Bloodquack', + passiveDescription: '+25% dmg below 30% HP, +50% below 15% HP', + specialName: 'Roundhouse Kick', + specialDescription: '30-45 dmg to one target. Cooldown resets on kill.', + specialCooldown: 4, + specialEnergyCost: 40, + }, + quackdini: { + id: 'quackdini', + name: 'Quackdini', + emoji: '🎩', + description: 'A trickster with 20% dodge chance. Mirror Decoy negates an attack and reflects 20 dmg.', + maxHp: 85, + maxEnergy: 120, + attackMod: 0, + defenseMod: 0, + passiveName: 'Smoke & Feathers', + passiveDescription: '20% dodge chance. After dodging, next attack deals +10 bonus dmg.', + specialName: 'Mirror Decoy', + specialDescription: 'Negate next incoming attack and reflect 20 dmg to attacker.', + specialCooldown: 3, + specialEnergyCost: 50, + }, + sirQuacksALot: { + id: 'sirQuacksALot', + name: 'Sir Quacks-A-Lot', + emoji: '🛡️', + description: 'A paladin tank. Blocking heals 8 HP (10 energy). Divine Shield absorbs 30 dmg.', + maxHp: 120, + maxEnergy: 90, + attackMod: -0.1, + defenseMod: 0.2, + passiveName: 'Holy Plumage', + passiveDescription: 'Successful block heals 8 HP.', + specialName: 'Divine Shield', + specialDescription: 'Shield self or ally, absorbing next 30 dmg. Lasts 2 rounds.', + specialCooldown: 5, + specialEnergyCost: 45, + }, + drQuackenstein: { + id: 'drQuackenstein', + name: 'Dr. Quackenstein', + emoji: '🧪', + description: 'A warlock. All attacks apply poison (4 dmg/round, 3 turns). Plague Bomb hits all enemies for 15.', + maxHp: 90, + maxEnergy: 130, + attackMod: 0, + defenseMod: 0, + passiveName: 'Toxic Aura', + passiveDescription: 'Attacks apply 3-round poison (4 dmg/round, stacks up to 2x).', + specialName: 'Plague Bomb', + specialDescription: '15 dmg to ALL enemies + apply poison.', + specialCooldown: 5, + specialEnergyCost: 55, + }, +} + +export const CLASS_LORE: Record = { + mallardNorris: + 'Born during the Great Bread Famine, Mallard Norris learned to fight before he learned to swim. They say he once roundhouse kicked a loaf of bread so hard it became toast. His bloodline carries the fury of a thousand angry geese, and when his health drops low, something primal awakens — the Bloodquack. Chuck Norris himself adopted this fighting style after losing a staring contest with the original Mallard.', + quackdini: + 'The legendary stage magician who vanished during his own show and never came back. Some say Quackdini never left — he simply became invisible and has been stealing breadcrumbs from the audience ever since. His Mirror Decoy technique was perfected after decades of evading angry theater critics. "The wing is quicker than the eye," he whispers, before disappearing in a puff of feathers.', + sirQuacksALot: + 'Ordained by The Great Mallard during the Third Breadcrumb Crusade, Sir Quacks-A-Lot took a vow to protect the innocent and block every attack aimed at his allies. His Holy Plumage glows with divine energy when he successfully blocks a heavy blow, healing his wounds through sheer righteousness. He speaks exclusively in noble proclamations and refers to everyone as "good sir" or "fair maiden duck."', + drQuackenstein: + 'Once a respected bread scientist at the Pond Eternal University, Dr. Quackenstein was expelled after his "experiments" turned the entire chemistry department green. Now he wages war with bubbling vials and toxic concoctions, poisoning everything he touches. His Plague Bomb is banned in 47 ponds, but he insists the side effects are "mostly cosmetic." His lab coat has never been washed.', +} + +export const CLASS_ALIASES: Record = { + mallard: 'mallardNorris', + norris: 'mallardNorris', + quackdini: 'quackdini', + trickster: 'quackdini', + sir: 'sirQuacksALot', + paladin: 'sirQuacksALot', + doc: 'drQuackenstein', + warlock: 'drQuackenstein', +} + +export const resolveClassAlias = (input: string): DuckClass | undefined => { + const normalized = input.toLowerCase().trim() + return ( + CLASS_ALIASES[normalized] ?? + (Object.keys(DUCK_CLASSES).includes(normalized) ? (normalized as DuckClass) : undefined) + ) +} + +const CLASS_TAGLINES: Record = { + mallardNorris: 'Hits harder the closer to death. Born to brawl.', + quackdini: 'Now you see me... actually, too late.', + sirQuacksALot: 'Holy tank. Blocks heal. Shields protect.', + drQuackenstein: "Everything is toxic. That's science.", +} + +export const formatClassList = (): string => { + return Object.values(DUCK_CLASSES) + .map( + (c) => + `${c.emoji} **${c.name}** — ${c.description}\n Passive: *${c.passiveName}* — ${c.passiveDescription}\n Special: *${c.specialName}* (${c.specialEnergyCost} energy, ${c.specialCooldown}r CD) — ${c.specialDescription}` + ) + .join('\n\n') +} + +export const formatCompactClassList = (): string => { + return Object.values(DUCK_CLASSES) + .map((c) => `${c.emoji} **${c.name}** — ${CLASS_TAGLINES[c.id]}`) + .join('\n') +} + +export const formatClassDetails = (duckClass: DuckClass): string => { + const c = DUCK_CLASSES[duckClass] + const atkSign = c.attackMod >= 0 ? '+' : '' + const defSign = c.defenseMod >= 0 ? '+' : '' + const aliases = Object.entries(CLASS_ALIASES) + .filter(([, v]) => v === c.id) + .map(([k]) => `\`${k}\``) + .join(', ') + + const lore = CLASS_LORE[duckClass] + + return [ + `${c.emoji} **${c.name}**`, + `> *${CLASS_TAGLINES[c.id]}*`, + '', + `*${lore}*`, + '', + `**Stats:** ${c.maxHp} HP | ${c.maxEnergy} Energy | ATK ${atkSign}${Math.round(c.attackMod * 100)}% | DEF ${defSign}${Math.round(c.defenseMod * 100)}%`, + `**Passive:** *${c.passiveName}* — ${c.passiveDescription}`, + `**Special:** *${c.specialName}* (${c.specialEnergyCost} energy, ${c.specialCooldown}r cooldown) — ${c.specialDescription}`, + '', + `**Aliases:** ${aliases}`, + ].join('\n') +} diff --git a/bots/quack-norris/src/lib/combat.ts b/bots/quack-norris/src/lib/combat.ts new file mode 100644 index 00000000000..14b799b3026 --- /dev/null +++ b/bots/quack-norris/src/lib/combat.ts @@ -0,0 +1,449 @@ +import { DUCK_CLASSES } from './classes' +import type { ActionType, CombatEvent, DuckClass, Player, StatusEffect } from './types' + +const randomBetween = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min +const chance = (percent: number) => Math.random() * 100 < percent + +// Energy costs per action type +export const ENERGY_COSTS: Record = { + light: 10, + heavy: 25, + block: 10, // was 15 — rebalanced to match light cost + rest: 0, + special: 0, // varies by class, handled separately + forfeit: 0, +} + +// Base damage ranges +const DAMAGE_RANGES: Record<'light' | 'heavy', { min: number; max: number }> = { + light: { min: 10, max: 18 }, + heavy: { min: 25, max: 38 }, +} + +const CRIT_CHANCE = 15 +const CRIT_MULTIPLIER = 1.5 +const BASE_DODGE_CHANCE = 5 +const QUACKDINI_DODGE_CHANCE = 20 +const COMBO_THRESHOLD = 3 +const COMBO_BONUS_DAMAGE = 15 +const ENERGY_REGEN_PER_ROUND = 8 // was 5 — buffed for better flow +const REST_ENERGY_RECOVERY = 30 +const REST_DAMAGE_INCREASE = 0.15 +const POISON_DAMAGE_PER_TICK = 4 +const POISON_MAX_STACKS = 2 +const POISON_DURATION = 3 +const BLESSED_DAMAGE_BONUS = 0.3 +const BLOCK_LIGHT_REDUCTION = 0.5 +const MAX_MULTIPLIER = 2.0 // cap on additive damage multipliers (before crit) + +export type DamageResult = { + damage: number + isCrit: boolean + isDodged: boolean + isBlocked: boolean + isBlockPartial: boolean + comboTriggered: boolean + poisonApplied: boolean +} + +export const getSpecialEnergyCost = (duckClass: DuckClass): number => { + return DUCK_CLASSES[duckClass].specialEnergyCost +} + +export const hasEnoughEnergy = (player: Player, actionType: ActionType): boolean => { + const cost = + actionType === 'special' && player.duckClass ? getSpecialEnergyCost(player.duckClass) : ENERGY_COSTS[actionType] + return (player.energy ?? 0) >= cost +} + +export const deductEnergy = (player: Player, actionType: ActionType): void => { + const cost = + actionType === 'special' && player.duckClass ? getSpecialEnergyCost(player.duckClass) : ENERGY_COSTS[actionType] + player.energy = Math.max(0, (player.energy ?? 0) - cost) +} + +export const regenerateEnergy = (player: Player): void => { + player.energy = Math.min(player.maxEnergy, (player.energy ?? 0) + ENERGY_REGEN_PER_ROUND) +} + +export const applyRest = (player: Player): void => { + player.energy = Math.min(player.maxEnergy, (player.energy ?? 0) + REST_ENERGY_RECOVERY) + addStatusEffect(player, { type: 'resting', turnsLeft: 1 }) +} + +export const getBloodquackMultiplier = (player: Player): number => { + if (player.duckClass !== 'mallardNorris') { + return 1 + } + const hpPercent = player.hp / player.maxHp + if (hpPercent <= 0.15) { + return 1.5 + } + if (hpPercent <= 0.3) { + return 1.25 + } + return 1 +} + +export const getDodgeChance = (player: Player): number => { + return player.duckClass === 'quackdini' ? QUACKDINI_DODGE_CHANCE : BASE_DODGE_CHANCE +} + +export const attemptDodge = (target: Player): boolean => { + return chance(getDodgeChance(target)) +} + +export const calculateDamage = ( + attacker: Player, + target: Player, + actionType: 'light' | 'heavy', + isBlocking: boolean +): DamageResult => { + const result: DamageResult = { + damage: 0, + isCrit: false, + isDodged: false, + isBlocked: false, + isBlockPartial: false, + comboTriggered: false, + poisonApplied: false, + } + + // Fog Bomb: auto-dodge all attacks this round + if (hasStatusEffect(target, 'dodgeAll')) { + result.isDodged = true + return result + } + + // Check dodge (only non-blocked attacks can be dodged) + if (!hasStatusEffect(target, 'exposed') && attemptDodge(target)) { + result.isDodged = true + // Quackdini dodge bonus: mark for next attack + if (target.duckClass === 'quackdini') { + addStatusEffect(target, { type: 'inspired', turnsLeft: 1 }) + } + return result + } + + // Check block — heavy attacks fully blocked, light attacks halved + if (isBlocking) { + if (actionType === 'heavy') { + result.isBlocked = true + return result + } + // Light attacks vs block: 50% damage reduction + result.isBlockPartial = true + } + + // Calculate base damage + const range = DAMAGE_RANGES[actionType] + let damage = randomBetween(range.min, range.max) + + // Build additive multiplier (capped at MAX_MULTIPLIER) + let multiplier = 1.0 + + // Attacker class modifier + if (attacker.duckClass) { + const classDef = DUCK_CLASSES[attacker.duckClass] + multiplier += classDef.attackMod + } + + // Bloodquack (Mallard Norris passive) + const bloodquack = getBloodquackMultiplier(attacker) + if (bloodquack > 1) { + multiplier += bloodquack - 1 + } + + // Blessed bonus (+30% dmg) + if (hasStatusEffect(attacker, 'blessed')) { + multiplier += BLESSED_DAMAGE_BONUS + } + + // Target defense modifier (subtractive) + if (target.duckClass) { + const targetClass = DUCK_CLASSES[target.duckClass] + multiplier -= targetClass.defenseMod + } + + // Resting targets take extra damage + if (hasStatusEffect(target, 'resting')) { + multiplier += REST_DAMAGE_INCREASE + } + + // Cap multiplier before crit + multiplier = Math.min(multiplier, MAX_MULTIPLIER) + + // Apply multiplier + damage = Math.round(damage * multiplier) + + // Block partial reduction (light vs block) + if (result.isBlockPartial) { + damage = Math.round(damage * BLOCK_LIGHT_REDUCTION) + } + + // Inspired bonus (all classes — primarily granted to Quackdini on dodge) + if (hasStatusEffect(attacker, 'inspired')) { + damage += 10 + removeStatusEffect(attacker, 'inspired') + } + + // DamageBoost bonus (from items or combo — works for ALL classes) + const damageBoostEffect = (attacker.statusEffects ?? []).find((e) => e.type === 'damageBoost') + if (damageBoostEffect) { + damage += damageBoostEffect.stacks ?? 10 + removeStatusEffect(attacker, 'damageBoost') + } + + // Critical hit (applied AFTER multiplier cap) + if (chance(CRIT_CHANCE)) { + damage = Math.round(damage * CRIT_MULTIPLIER) + result.isCrit = true + } + + // Combo check + if (attacker.consecutiveTargetId === target.discordUserId) { + attacker.consecutiveHits = (attacker.consecutiveHits ?? 0) + 1 + if (attacker.consecutiveHits >= COMBO_THRESHOLD) { + damage += COMBO_BONUS_DAMAGE + result.comboTriggered = true + addStatusEffect(attacker, { type: 'damageBoost', turnsLeft: 2, stacks: 15 }) + attacker.consecutiveHits = 0 + } + } else { + attacker.consecutiveTargetId = target.discordUserId + attacker.consecutiveHits = 1 + } + + // Apply damage + result.damage = Math.max(1, damage) + target.hp = Math.max(0, target.hp - result.damage) + + // Dr. Quackenstein toxic aura + if (attacker.duckClass === 'drQuackenstein') { + applyPoison(target) + result.poisonApplied = true + } + + return result +} + +// --- Special Abilities --- + +export type SpecialResult = { + events: CombatEvent[] + kills: string[] +} + +export const executeSpecial = (caster: Player, target: Player | undefined, allPlayers: Player[]): SpecialResult => { + const events: CombatEvent[] = [] + const kills: string[] = [] + + if (!caster.duckClass) { + return { events, kills } + } + + switch (caster.duckClass) { + case 'mallardNorris': { + if (!target || !target.alive) { + break + } + const damage = randomBetween(30, 45) + const finalDamage = Math.round(damage * getBloodquackMultiplier(caster)) + target.hp = Math.max(0, target.hp - finalDamage) + events.push({ + text: `${caster.name} unleashes a ROUNDHOUSE KICK on ${target.name} for ${finalDamage} dmg!`, + type: 'special', + }) + // Apply exposed to surviving target + if (target.hp > 0) { + addStatusEffect(target, { type: 'exposed', turnsLeft: 1 }) + events.push({ + text: `${target.name} is left EXPOSED by the Roundhouse Kick!`, + type: 'status', + }) + } + if (target.hp <= 0) { + target.alive = false + kills.push(target.name) + caster.specialCooldown = 0 // Reset on kill + events.push({ + text: `${target.name} has been eliminated! Mallard Norris's cooldown resets!`, + type: 'elimination', + }) + } + break + } + + case 'quackdini': { + addStatusEffect(caster, { type: 'decoy', turnsLeft: 2 }) + events.push({ + text: `${caster.name} conjures a Mirror Decoy! The next attack will be reflected!`, + type: 'special', + }) + break + } + + case 'sirQuacksALot': { + // In free-for-all, always self-shield (no allies). Target shielding reserved for future team mode. + addStatusEffect(caster, { type: 'shielded', turnsLeft: 2, stacks: 30 }) + events.push({ + text: `${caster.name} casts Divine Shield on themselves! (absorbs up to 30 dmg)`, + type: 'special', + }) + break + } + + case 'drQuackenstein': { + const enemies = allPlayers.filter((p) => p.alive && p.discordUserId !== caster.discordUserId) + // Scale AoE damage: cap total output at ~30 to prevent domination in small games + const perTargetDmg = enemies.length > 0 ? Math.min(15, Math.round(30 / enemies.length)) : 15 + for (const enemy of enemies) { + enemy.hp = Math.max(0, enemy.hp - perTargetDmg) + applyPoison(enemy) + if (enemy.hp <= 0 && enemy.alive) { + enemy.alive = false + kills.push(enemy.name) + } + } + events.push({ + text: `${caster.name} throws a PLAGUE BOMB! All enemies take ${perTargetDmg} dmg and are poisoned!`, + type: 'special', + }) + break + } + default: + break + } + + return { events, kills } +} + +// --- Status Effects --- + +export const addStatusEffect = (player: Player, effect: StatusEffect): void => { + if (!player.statusEffects) { + player.statusEffects = [] + } + + if (effect.type === 'poison') { + const existing = player.statusEffects.find((e) => e.type === 'poison') + if (existing) { + existing.stacks = Math.min(POISON_MAX_STACKS, (existing.stacks ?? 1) + 1) + existing.turnsLeft = POISON_DURATION + return + } + } + + // For non-stackable effects, refresh duration + const existing = player.statusEffects.find((e) => e.type === effect.type) + if (existing) { + existing.turnsLeft = effect.turnsLeft + if (effect.stacks !== undefined) { + existing.stacks = effect.stacks + } + return + } + + player.statusEffects.push(effect) +} + +export const removeStatusEffect = (player: Player, type: string): void => { + if (!player.statusEffects) { + return + } + player.statusEffects = player.statusEffects.filter((e) => e.type !== type) +} + +export const hasStatusEffect = (player: Player, type: string): boolean => { + return (player.statusEffects ?? []).some((e) => e.type === type) +} + +export const tickStatusEffects = (players: Player[]): CombatEvent[] => { + const events: CombatEvent[] = [] + + for (const player of players) { + if (!player.alive || !player.statusEffects) { + continue + } + + // Apply poison damage + const poison = player.statusEffects.find((e) => e.type === 'poison') + if (poison) { + const poisonDmg = POISON_DAMAGE_PER_TICK * (poison.stacks ?? 1) + player.hp = Math.max(0, player.hp - poisonDmg) + events.push({ text: `${player.name} takes ${poisonDmg} poison damage!`, type: 'status' }) + } + + // Decrement all durations + player.statusEffects = player.statusEffects + .map((e) => ({ ...e, turnsLeft: e.turnsLeft - 1 })) + .filter((e) => e.turnsLeft > 0) + } + + return events +} + +export const applyPoison = (target: Player): void => { + addStatusEffect(target, { type: 'poison', turnsLeft: POISON_DURATION, stacks: 1 }) +} + +// --- Shield (Decoy / Divine Shield) interaction --- + +export const processShieldedAttack = ( + attacker: Player, + target: Player, + damage: number +): { absorbed: boolean; reflected: boolean; reflectDamage: number } => { + // Quackdini decoy: negate + reflect 20 + if (hasStatusEffect(target, 'decoy')) { + removeStatusEffect(target, 'decoy') + target.hp = Math.min(target.maxHp, target.hp + damage) // Undo damage + attacker.hp = Math.max(0, attacker.hp - 20) + return { absorbed: true, reflected: true, reflectDamage: 20 } + } + + // Divine Shield / Shield Token: absorb up to stacks value (or 30 for Divine Shield) + if (hasStatusEffect(target, 'shielded')) { + const shieldEffect = (target.statusEffects ?? []).find((e) => e.type === 'shielded') + const absorptionCap = shieldEffect?.stacks ?? 30 // Divine Shield defaults to 30 + const absorbed = Math.min(damage, absorptionCap) + target.hp = Math.min(target.maxHp, target.hp + absorbed) // Undo absorbed portion + removeStatusEffect(target, 'shielded') + return { absorbed: true, reflected: false, reflectDamage: 0 } + } + + return { absorbed: false, reflected: false, reflectDamage: 0 } +} + +// --- Combo Reset --- + +export const resetComboCounters = (players: Player[]): void => { + for (const player of players) { + player.consecutiveHits = 0 + player.consecutiveTargetId = undefined + } +} + +// --- Cooldown Management --- + +export const decrementCooldowns = (players: Player[]): void => { + for (const player of players) { + if (player.specialCooldown > 0) { + player.specialCooldown-- + } + } +} + +// --- HP Bar Rendering --- + +export const renderHpBar = (current: number, max: number, length = 10): string => { + const filled = Math.round((current / max) * length) + const empty = length - filled + return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]` +} + +export const renderEnergyBar = (current: number, max: number, length = 8): string => { + const filled = Math.round((current / max) * length) + const empty = length - filled + return `[${'⚡'.repeat(filled)}${'·'.repeat(empty)}]` +} diff --git a/bots/quack-norris/src/lib/command-context.ts b/bots/quack-norris/src/lib/command-context.ts new file mode 100644 index 00000000000..331ba4bdf9e --- /dev/null +++ b/bots/quack-norris/src/lib/command-context.ts @@ -0,0 +1,48 @@ +import type { actions } from '@botpress/runtime' +import type { PlayersTable } from '../tables/Players' +import type { QuestProgress, QuestObjectiveType, getQuestById } from './quests' + +export type ProfileRow = Awaited>['rows'][0] + +export type AwaitingChoice = 'encounter' | 'travel' | 'quest_choice' | 'quest_accept' | 'shop' | 'none' + +export type InteractionState = { + awaitingChoice: AwaitingChoice + pendingQuestId?: string + pendingNpcId?: string +} + +export type SendFn = (text: string, showHud?: boolean) => Promise + +export type SetStateFn = (profile: ProfileRow, update: Partial) => Promise + +export type QuestAdvanceFn = (eventType: QuestObjectiveType, eventTarget?: string) => Promise + +export type CompleteQuestFn = ( + profile: ProfileRow, + quest: QuestProgress, + def: ReturnType & object +) => Promise + +export type CommandContext = { + command: string + discordUserId: string + channelId: string + guildId: string | undefined + args: string[] + message: { id: string; userId: string; tags: Record } + sendText: SendFn + loadProfile: () => Promise + setInteractionState: SetStateFn + getDisplayName: () => Promise + advanceQuestObjectives: QuestAdvanceFn + completeQuest: CompleteQuestFn + withProfile: (mutate: (profile: ProfileRow) => ProfileRow | Promise) => Promise + invalidateCache: () => void + stateRef: { activeGameId?: string } + convTags: Record + actions: typeof actions + conversation: { id: string; send: (msg: { type: string; payload: { text: string } }) => Promise } +} + +export type CommandHandler = (ctx: CommandContext) => Promise diff --git a/bots/quack-norris/src/lib/encounters.ts b/bots/quack-norris/src/lib/encounters.ts new file mode 100644 index 00000000000..aa7bd8257bf --- /dev/null +++ b/bots/quack-norris/src/lib/encounters.ts @@ -0,0 +1,1092 @@ +import type { ItemType } from './items' +import type { LocationId } from './locations' + +export type EncounterType = 'npc' | 'discovery' | 'lore' + +export type EncounterChoice = { + label: string + outcome: string + reward?: ItemType + /** Breadcrumb gain (positive) or loss (negative) from this choice */ + breadcrumbDelta?: number + /** XP gain (positive) or loss (negative) from this choice */ + xpDelta?: number +} + +export type EncounterDefinition = { + id: string + type: EncounterType + location: LocationId | 'any' + narrative: string + choices: EncounterChoice[] + questTrigger?: { questId: string; stepId: string } +} + +const pick = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)]! + +// --- NPC Encounters --- + +const NPC_ENCOUNTERS: EncounterDefinition[] = [ + { + id: 'duchess_judgment', + type: 'npc', + location: 'coliseum', + narrative: + '**Duchess Featherington** glides toward you, monocle gleaming. *"My my, another aspiring gladiator? Let me assess your... potential."* She circles you with devastating elegance.\n\n*"I shall offer you a choice, darling."*', + choices: [ + { + label: 'Bow respectfully', + outcome: + 'You bow low. The Duchess nods approvingly. *"Manners. How refreshingly rare in this cesspool of violence."* She tosses you a small vial.', + reward: 'hpPotion', + breadcrumbDelta: 3, + }, + { + label: 'Challenge her to a staring contest', + outcome: + 'You lock eyes with the Duchess. Three minutes pass. Your eyes water. She does not blink once. *"Amusing,"* she says, and hands you a consolation prize.', + reward: 'energyDrink', + breadcrumbDelta: -3, + }, + { + label: 'Compliment her feathers', + outcome: + '*"Obviously they are magnificent. But I appreciate the observation."* She reaches into her gown and produces a shard of her legendary vanity mirror. *"For a duck with taste."*', + reward: 'mirrorShard', + xpDelta: 8, + }, + ], + }, + { + id: 'duchess_gala', + type: 'npc', + location: 'coliseum', + narrative: + '**Duchess Featherington** sweeps past in a gown made entirely of breadcrumbs. *"Darling! You simply MUST attend my soirée tonight. The Coliseum\'s upper gallery. Formal attire required — and by formal, I mean feathers."*\n\nShe hands you an invitation on gold-leaf bread.', + choices: [ + { + label: 'Attend in your finest feathers', + outcome: + 'You arrive looking spectacular. The Duchess introduces you to her inner circle. *"This one has potential,"* she declares. A servant hands you a gift box.', + reward: 'damageBoost', + breadcrumbDelta: -4, + }, + { + label: 'Show up fashionably late', + outcome: + 'The party is winding down when you arrive. *"Hmph. Late, but at least you came."* She tosses you a leftover party favor — it glows faintly.', + reward: 'shieldToken', + }, + { + label: 'RSVP "No" with a counter-invitation', + outcome: + 'The Duchess reads your note and laughs — actually laughs. *"The audacity! I respect it."* She sends you a gift as a peace offering.', + reward: 'hpPotion', + xpDelta: 10, + }, + ], + }, + { + id: 'chad_theft', + type: 'npc', + location: 'puddle', + narrative: + '**Chad Gullsworth** swoops down and lands on the Honda Civic. *"NICE BREADCRUMBS YOU GOT THERE! BE A SHAME IF SOMEONE... REDISTRIBUTED THEM!"* He cackles and steals a breadcrumb from your pocket.', + choices: [ + { + label: 'Chase him', + outcome: + 'You chase Chad around the parking lot for five minutes. He drops something shiny in his escape. Victory through cardio!', + reward: 'damageBoost', + breadcrumbDelta: -5, + }, + { + label: 'Offer him more breadcrumbs', + outcome: + "Chad is so confused by generosity that he drops everything he's carrying and flies away in a panic. You pick up a potion he left behind.", + reward: 'hpPotion', + breadcrumbDelta: 4, + }, + { + label: 'Ignore him completely', + outcome: + 'You stare blankly ahead. Chad gets increasingly agitated. *"HEY! HEY ARE YOU LISTENING?!"* He throws a coffee at you in frustration. It\'s still warm.', + reward: 'energyDrink', + }, + ], + }, + { + id: 'chad_rematch', + type: 'npc', + location: 'puddle', + narrative: + '**Chad Gullsworth** is back, perched atop the Honda Civic\'s roof with arms crossed. *"YOU AGAIN? Listen, last time was a FLUKE. I challenge you to a BREADCRUMB DUEL! Three breadcrumbs each. Whoever eats theirs fastest wins. I\'ve been TRAINING."*\n\nHe produces six breadcrumbs from behind his back.', + choices: [ + { + label: 'Accept the duel', + outcome: + 'You vacuum down three breadcrumbs in 1.2 seconds. Chad stares, mouth full, breadcrumb stuck to his beak. *"...HOW?!"* He drops his prize in shock.', + reward: 'damageBoost', + breadcrumbDelta: -6, + }, + { + label: 'Challenge him to a different contest instead', + outcome: + '*"A STARING CONTEST?! YOU\'RE ON!"* Chad blinks immediately. He throws a tantrum and kicks over his own stash. You pick up the pieces.', + reward: 'energyDrink', + xpDelta: 8, + }, + { + label: 'Steal his breadcrumbs mid-speech', + outcome: + 'While Chad monologues about his training regimen, you casually eat all six breadcrumbs. He turns around to an empty table. *"...I respect the hustle."* In his rage, he hurls a smoking sphere at you — but it\'s actually useful.', + reward: 'quackGrenade', + xpDelta: 15, + }, + ], + }, + { + id: 'bigmouth_wager', + type: 'npc', + location: 'quackatoa', + narrative: + '**Big Mouth McGee** surfaces from the volcanic lake, steam rising from his enormous beak. *"WELL WELL WELL! A visitor! Tell you what — I\'ll make you a deal. I\'ve got something good in my pouch. But you gotta earn it."*', + choices: [ + { + label: 'Accept the challenge', + outcome: + 'McGee spits a jet of boiling water. You dodge! *"Not bad, not bad!"* He tosses you a glowing bread from his pouch. It\'s warm but intact.', + reward: 'damageBoost', + breadcrumbDelta: -5, + }, + { + label: 'Ask what the catch is', + outcome: + '*"The CATCH?! HA! The catch is I\'m a PELICAN! Get it?!"* He laughs so hard a feather token falls out of his beak.', + reward: 'shieldToken', + breadcrumbDelta: 3, + }, + { + label: 'Try to peek in his pouch', + outcome: + 'Big Mouth snaps his beak shut. *"RUDE!"* But in the commotion, an energy drink rolls out. Finders keepers.', + reward: 'energyDrink', + breadcrumbDelta: -3, + }, + ], + }, + { + id: 'bigmouth_riddle', + type: 'npc', + location: 'quackatoa', + narrative: + '**Big Mouth McGee** rises from the volcanic depths, lava dripping from his enormous beak. *"RIDDLE TIME! Answer correctly and you get a PRIZE. Answer wrong and you get a DIFFERENT prize. I haven\'t decided which is better."*\n\nHe clears his throat. *"What has bread but no mouth, feathers but no wings, and is currently standing in a volcano?"*', + choices: [ + { + label: '"A breadcrumb golem?"', + outcome: + '*"CLOSE ENOUGH!"* McGee slaps you on the back (nearly knocking you into lava) and hands you a glowing potion. *"You win the good prize! ...I think."*', + reward: 'hpPotion', + }, + { + label: '"You, Big Mouth."', + outcome: + 'McGee\'s eyes go wide. *"...That\'s... actually correct. I AM standing in a volcano. I have bread in my pouch. And my wings are decorative at best."* He hands you something shiny, looking existential.', + reward: 'shieldToken', + }, + { + label: '"Is this a trick question?"', + outcome: + '*"EVERYTHING is a trick question when you\'re a pelican in a volcano!"* He cackles and tosses you the "wrong" prize. It seems fine.', + reward: 'damageBoost', + }, + ], + }, + { + id: 'gerald_wisdom', + type: 'npc', + location: 'highway', + narrative: + 'Gerald the flying goose banks left suddenly. You grab onto his feathers. *"HONK."* He seems to want to tell you something. He gestures with his wing toward a cloud formation that looks suspiciously like a breadcrumb.', + choices: [ + { + label: 'Reach for the cloud', + outcome: + "You lean over and... it's actually a floating bread poultice, held aloft by pure Quackverse magic. Gerald honks approvingly.", + reward: 'hpPotion', + }, + { + label: 'Ask Gerald for life advice', + outcome: + 'Gerald turns his head 180 degrees to look at you. *"HONK."* You feel strangely energized. Was that... wisdom?', + reward: 'energyDrink', + }, + { + label: 'Do a barrel roll', + outcome: + 'You and Gerald do a barrel roll through the clouds. When you stabilize, you find a golden feather caught in your wing. Gerald looks proud.', + reward: 'shieldToken', + }, + ], + }, + { + id: 'gerald_detour', + type: 'npc', + location: 'highway', + narrative: + 'Gerald takes an abrupt detour into a thundercloud. Lightning crackles. Rain pelts your face. Gerald seems completely unbothered.\n\n*"HONK."*\n\nThrough the storm, you see three glowing objects suspended in mid-air. Gerald circles them patiently.', + choices: [ + { + label: 'Grab the red one', + outcome: + "You snatch the red orb. It's warm — almost hot. It pulses with aggressive energy. Gerald honks once, approving of your fighting spirit.", + reward: 'damageBoost', + breadcrumbDelta: -4, + }, + { + label: 'Grab the blue one', + outcome: + 'The blue orb is cool to the touch and hums with restorative energy. Gerald nods. *"HONK."* That\'s Gerald for "wise choice."', + reward: 'hpPotion', + breadcrumbDelta: 3, + }, + { + label: 'Grab all three at once', + outcome: + 'You lunge for all three. They collide in your wings and merge into a swirling sphere of mist. Gerald honks approvingly — *"HONK!"* — this was the real treasure. A **Fog Bomb**, condensed from the storm itself.', + reward: 'fogBomb', + xpDelta: 10, + }, + ], + }, + { + id: 'harold_stare', + type: 'npc', + location: 'parkBench', + narrative: + '**Harold** the old man reaches into his coat pocket. He pulls out a piece of bread. He tears it slowly. Deliberately. He holds a piece toward you.\n\nHis eyes say: *"Take it. But know this changes nothing between us."*', + choices: [ + { + label: 'Take the bread gratefully', + outcome: + 'You take the bread. Harold nods once. The bread is enchanted — it radiates warmth and smells like victory.', + reward: 'hpPotion', + }, + { + label: 'Sit next to Harold in silence', + outcome: + 'You sit. Minutes pass. Harold does not move. You do not move. Eventually, a golden feather drifts down from somewhere. Harold seems satisfied.', + reward: 'shieldToken', + }, + { + label: 'Do a little dance for Harold', + outcome: + "You dance your best duck dance. Harold's expression does not change. But a single tear rolls down his cheek. He hands you his special bread.", + reward: 'damageBoost', + }, + ], + }, + { + id: 'harold_chess', + type: 'npc', + location: 'parkBench', + narrative: + '**Harold** has set up a chessboard on the park bench. He does not look up as you approach. The pieces are breadcrumbs — white sourdough vs dark rye.\n\nHe pushes a pawn forward. It is your move. Harold has been waiting.', + choices: [ + { + label: "Play Harold's game", + outcome: + 'You play for forty minutes. Harold wins in six moves, somehow. The remaining thirty-four minutes were him waiting for you to realize it. He slides a potion across the board. *"For trying."*', + reward: 'hpPotion', + breadcrumbDelta: 2, + xpDelta: 5, + }, + { + label: 'Flip the board', + outcome: + 'Breadcrumbs scatter everywhere. Harold blinks. For the first time in recorded history, his expression changes — the faintest hint of a smile. He reaches into his coat and hands you something. *"Bold."*', + reward: 'damageBoost', + breadcrumbDelta: -5, + }, + { + label: 'Eat one of the chess pieces', + outcome: + "You eat the white queen. It's delicious. Harold stares at you for a long time. Then he slowly, deliberately, eats the black king. You are now bonded for life. He hands you a gift.", + reward: 'energyDrink', + xpDelta: 8, + }, + ], + }, + // --- Frostbeak (Frozen Pond NPC) --- + { + id: 'frostbeak_thaw', + type: 'npc', + location: 'frozenPond', + narrative: + 'A duck-shaped ice sculpture stands in the center of the pond. As you approach, its eyes blink. *"Oh! A visitor! I\'m **Frostbeak** — I\'ve been frozen here for... what century is this?"*\n\nThe ice around them cracks slightly. *"I could use some help thawing out. Or you could just... talk to me. It gets lonely being a popsicle."*', + choices: [ + { + label: 'Help them thaw', + outcome: + 'You chip away at the ice with your beak. Frostbeak stretches their wings for the first time in centuries. *"FREEDOM! Oh, that\'s nice. Here — take this. I\'ve been keeping it warm. ...Well, cold. Same thing."*', + reward: 'hpPotion', + breadcrumbDelta: 4, + }, + { + label: 'Ask about the century question', + outcome: + '*"When I froze, ducks still used swords made of breadsticks. The Coliseum hadn\'t been built. Chuck Norris was just a regular guy with slightly above-average roundhouse kicks."* They hand you an ancient artifact from beneath the ice.', + reward: 'shieldToken', + xpDelta: 10, + }, + { + label: 'Lick the ice', + outcome: + "Your tongue gets stuck. Frostbeak laughs — the first warmth they've felt in ages. The ice cracks and a strange magnetic device tumbles free. *\"That's been stuck in there since the Third Age. Take it — it attracts breadcrumbs like you wouldn't believe.\"*", + reward: 'breadcrumbMagnet', + breadcrumbDelta: -4, + }, + ], + }, + { + id: 'frostbeak_memories', + type: 'lore', + location: 'frozenPond', + narrative: + '**Frostbeak** sits on a (slightly melted) ice throne, gazing at the frozen horizon. *"You know, I remember the First Quacktament. Before the classes. Before the chaos events. It was just... ducks hitting each other with bread."*\n\nThey sigh wistfully. *"Simpler times."*', + choices: [ + { + label: 'Ask about the First Quacktament', + outcome: + '*"The first champion was a duck named \'Crumbles.\' Won with nothing but a stale baguette and an attitude problem."* Frostbeak hands you a relic — a piece of that very baguette, preserved in ice.', + reward: 'damageBoost', + }, + { + label: 'Share a warm breadcrumb', + outcome: + 'You offer a breadcrumb. Frostbeak holds it like it\'s the most precious thing in the world. A tear freezes on their cheek. *"Thank you. Here — it\'s dangerous to go alone."*', + reward: 'hpPotion', + }, + ], + }, +] + +// --- Discovery Encounters --- + +const DISCOVERY_ENCOUNTERS: EncounterDefinition[] = [ + { + id: 'hidden_stash', + type: 'discovery', + location: 'any', + narrative: + 'A loose breadcrumb tile shifts under your webbed foot. Beneath it: a small iron box, rusted shut and etched with a faded inscription: *"Property of Crumbles — First Champion of the Quacktament."*\n\nThe lock is ancient but the contents hum with residual arena magic. Whatever Crumbles stashed here, they meant for it to be found.', + choices: [ + { + label: 'Pry it open carefully', + outcome: + "The lid creaks open. Inside, wrapped in wax paper that smells like a bakery from another century: a perfectly preserved **Bread Poultice**. Crumbles' emergency supply, waiting all this time for a worthy duck.", + reward: 'hpPotion', + breadcrumbDelta: 3, + }, + { + label: 'Smash it open with your wing', + outcome: + "You bring your wing down HARD. The box shatters and golden crumbs scatter. Among the debris: a **Bread of Fury**, still crackling with Crumbles' legendary aggression. The first champion's fighting spirit lives on.", + reward: 'damageBoost', + breadcrumbDelta: -5, + }, + { + label: 'Check for traps first', + outcome: + "Smart — Crumbles was paranoid. A tiny spring-loaded breadcrumb launcher nearly takes your eye out. You disarm it and find a **Pond Water Espresso** hidden in a false bottom. Crumbles didn't just fight hard — they planned hard.", + reward: 'energyDrink', + xpDelta: 8, + }, + ], + }, + { + id: 'wandering_vendor', + type: 'discovery', + location: 'any', + narrative: + '🧥 **Trenchbill** materializes from the shadows, trenchcoat flapping. An unlit cigarette dangles from his beak. *"Psst. You. Yeah, you. I got goods."*\n\nHe opens his coat to reveal an assortment of glowing wares. *"No refunds, no returns, no eye contact."*', + choices: [ + { + label: 'Browse the health section', + outcome: + '*"Bread Poultice. Medicinal grade. Fell off a wagon."* Trenchbill hands you a neatly wrapped compress. His trenchcoat rustles ominously.', + reward: 'hpPotion', + }, + { + label: 'Ask for the good stuff', + outcome: + "Trenchbill looks both ways. Then up. Then down. *\"Chuck's Feather Token. Don't ask how I got it. Don't tell Chuck.\"* He wraps it in old newspaper.", + reward: 'shieldToken', + }, + { + label: 'Haggle aggressively', + outcome: + 'After a heated negotiation involving bread futures and feather derivatives, Trenchbill concedes. *"Fine. Pond Water Espresso. Triple-distilled. My personal stash."* He looks genuinely pained.', + reward: 'energyDrink', + }, + ], + }, + { + id: 'training_dummy', + type: 'discovery', + location: 'coliseum', + narrative: + 'In a shadowed alcove of the Coliseum stands **Old Ironsides** — a battle-scarred training dummy built from petrified breadcrumbs and rusted arena chain. Unlike Practice Dummy #47 by the gates, Old Ironsides has *history*. Deep gouges from Mallard Norris roundhouses. Acid burns from Dr. Quackenstein\'s earliest plague experiments. A chunk missing from its shoulder where, legend says, Chuck Norris once sneezed in its general direction.\n\nA faded plaque reads: *"I have trained champions. I have outlasted them all."*\n\nSomething glints behind its battered frame.', + choices: [ + { + label: 'Strike it with respect', + outcome: + "You deliver a clean hit. Old Ironsides absorbs the blow without flinching — it has taken worse. A hidden compartment clicks open in its chest, revealing a **Bread of Fury** worn smooth by generations of warriors. You've earned it.", + reward: 'damageBoost', + breadcrumbDelta: -3, + }, + { + label: 'Salute and hold your ground', + outcome: + "You raise your wing in a warrior's salute. Old Ironsides does not move. It never moves. But a golden feather drifts down from a crack in the ceiling above — as if the Coliseum itself rewards your discipline.", + reward: 'shieldToken', + breadcrumbDelta: 3, + }, + { + label: 'Investigate behind it', + outcome: + 'You squeeze behind Old Ironsides and find a dusty **Pond Water Espresso** wedged in a crack. The label reads *"Best before 1847."* It\'s probably fine. Old Ironsides has been guarding it all this time.', + reward: 'energyDrink', + xpDelta: 5, + }, + ], + }, +] + +// --- Lore Encounters --- + +const LORE_ENCOUNTERS: EncounterDefinition[] = [ + { + id: 'chuck_norris_fact', + type: 'lore', + location: 'any', + narrative: + 'A stone tablet emerges from the ground. Inscribed upon it is an ancient **Chuck Norris Fact**:\n\n*"Chuck Norris once roundhouse kicked a duck so hard it evolved into a swan. The swan was immediately disqualified from the Quacktament."*\n\nYou feel inspired by this knowledge.', + choices: [ + { + label: 'Meditate on the wisdom', + outcome: + 'You sit cross-legged before the tablet and close your eyes. The Chuck Norris Fact reverberates through your being. When you open your eyes, hours have passed — and a **Pond Water Espresso** has materialized beside you, condensed from pure concentrated inspiration.', + reward: 'energyDrink', + xpDelta: 10, + }, + { + label: 'Try to replicate the kick', + outcome: + "You attempt a roundhouse kick. Your foot connects with nothing, you spin three times, and crash into the tablet. It cracks — and from inside, a **Bread of Fury** tumbles out, still radiating the heat of Chuck's original kick. Some things are meant to be found the hard way.", + reward: 'damageBoost', + breadcrumbDelta: -4, + }, + ], + }, + { + id: 'great_mallard_vision', + type: 'lore', + location: 'any', + narrative: + 'The air shimmers. For a brief moment, you see **The Great Mallard** — the cosmic duck who sneezed the universe into existence. They wink at you.\n\n*"Quack,"* they say. It means everything.', + choices: [ + { + label: 'Quack back', + outcome: + "You quack into the void. The sound echoes — not off walls, but off *time itself*. The Great Mallard nods slowly, and a golden feather materializes in your wing, warm with cosmic energy. You've been blessed by the sneeze that started everything.", + reward: 'shieldToken', + xpDelta: 5, + }, + { + label: 'Ask about the meaning of quack', + outcome: + 'The Great Mallard tilts their head. The universe holds its breath. *"It means \'bread,\'"* they say softly. *"It has always meant bread."* A Bread Poultice appears in your wings, still warm from the beginning of time. The Mallard fades, leaving behind the faint scent of sourdough.', + reward: 'hpPotion', + xpDelta: 5, + }, + ], + }, + { + id: 'arena_history', + type: 'lore', + location: 'coliseum', + narrative: + 'You find an old mural depicting the **First Quacktament**. The ducks in the painting are using breadsticks as swords. One duck — labeled *"The Original Mallard Norris"* — is roundhouse kicking three opponents simultaneously.\n\nA plaque reads: *"In the beginning, there was bread. And then there was violence."*', + choices: [ + { + label: 'Pay respects', + outcome: + 'You bow low before the mural. The painted eyes of the Original Mallard Norris seem to glow. A warm breeze sweeps through the corridor and a **Bread Poultice** slides from behind the frame, wrapped in ancient linen. The inscription on the wrapper reads: *"For those who remember."*', + reward: 'hpPotion', + xpDelta: 5, + }, + { + label: 'Take a selfie with the mural', + outcome: + "The flash disturbs a sleeping bat-duck behind the mural. It shrieks, drops an energy drink, and crashes into the painting — revealing a hidden **Mirror Shard** embedded in the wall behind the Original Mallard's roundhouse kick. The bat-duck is fine. Probably.", + reward: 'mirrorShard', + }, + ], + }, + { + id: 'attenbird_documentary', + type: 'lore', + location: 'any', + narrative: + 'A distinguished-looking duck in a tweed jacket and tiny monocle appears, flanked by an invisible camera crew.\n\n*"Ah, the remarkable Quacktament participant,"* whispers **Sir David Attenbird**. *"Observe how it navigates its environment with a mixture of confidence and mild confusion. Truly fascinating."*\n\nHe turns to you directly. *"Would you mind terribly if I documented your journey? For science, of course."*', + choices: [ + { + label: 'Strike a heroic pose', + outcome: + '*"Magnificent. The way the light catches your plumage... exquisite."* Sir David Attenbird hands you a signed copy of his book, *"The Breadcrumb Diaries."* Inside the cover: a hidden potion.', + reward: 'hpPotion', + }, + { + label: 'Ask him about the Quackverse', + outcome: + '*"The Quackverse is, in essence, what happens when an omnipotent duck sneezes. Every pond, every breadcrumb — all connected by threads of quacking."* He gives you a research stipend (it\'s an energy drink).', + reward: 'energyDrink', + }, + { + label: 'Attempt to narrate HIM', + outcome: + '*"And here we see the rare Sir David Attenbird, startled by his own reflection..."* He laughs so hard his monocle pops off. *"Well played."* He hands you a token of respect.', + reward: 'damageBoost', + }, + ], + }, +] + +// --- Tutorial Encounter --- + +export const TUTORIAL_ENCOUNTER: EncounterDefinition = { + id: 'tutorial_basics', + type: 'discovery', + location: 'coliseum', + narrative: + 'A battered straw dummy wobbles toward you on a rusty spring. Someone has painted angry eyes on its burlap face and scrawled **"Practice Dummy #47"** across its chest in breadcrumb paste. A tally of scratches covers its torso — each one a combatant who came before you.\n\nIt squeaks to a halt. Straw pokes out of a dozen old wounds. A faded ribbon pinned to its shoulder reads: *"Employee of the Month — 37 months running."*\n\nDummy #47 stares at you with its painted eyes, practically daring you to take a swing.', + choices: [ + { + label: 'Give it a light bonk', + outcome: + 'You bonk the dummy square in the face. It spins like a top, creaks to a stop, and a hidden compartment pops open in its chest. Inside: a **Bread Poultice**, carefully wrapped in old newspaper. Dummy #47 has been hoarding supplies.\n\nThe dummy wobbles back upright, seemingly satisfied. Another tally mark appears on its torso. You could swear it just winked.', + reward: 'hpPotion', + }, + { + label: 'Lean in and whisper to it', + outcome: + 'You lean close. The dummy\'s painted mouth seems to curve ever so slightly. From somewhere deep inside its straw body, a tinny voice crackles: *"The secret... is to never stop quacking."*\n\nA small **Bread Poultice** rolls out from a hole in its base, as if offered by an old friend. Dummy #47 has seen a thousand warriors. It knows things.', + reward: 'hpPotion', + }, + { + label: 'Challenge it to a duel', + outcome: + "You square up. Dummy #47 does not flinch. The straw rustles in a breeze that shouldn't exist indoors. You charge — the dummy doesn't move. You win. Obviously.\n\nBut as you turn away, something clatters to the floor behind you. A **Bread Poultice** has fallen from Dummy #47's shoulder ribbon. A gift from a veteran who has never won a single fight — and never once complained.", + reward: 'hpPotion', + }, + ], +} + +// --- Breadcrumb Vault Encounters --- + +const VAULT_ENCOUNTERS: EncounterDefinition[] = [ + { + id: 'vault_guardian', + type: 'npc', + location: 'breadcrumbVault', + narrative: + 'A massive breadcrumb golem blocks your path. Its eyes glow with ancient accounting magic. Carved into its chest: *"BALANCE THE LEDGER."*\n\nIt raises a fist made of compressed sourdough.', + choices: [ + { + label: 'Fight it with financial literacy', + outcome: + 'You shout depreciation schedules at the golem until it crumbles. Inside: a rare **Bread of Fury** stamped with the Duchess\'s seal. *"Approved."*', + reward: 'damageBoost', + breadcrumbDelta: -6, + }, + { + label: 'Offer a breadcrumb as tribute', + outcome: + 'The golem inspects your breadcrumb, nods solemnly, and steps aside. Behind it: a **Bread Poultice** wrapped in gold leaf. Premium healthcare.', + reward: 'hpPotion', + breadcrumbDelta: 3, + }, + { + label: 'Sneak past while it counts', + outcome: + 'The golem is muttering "...four hundred and twelve... four hundred and thirteen..." You tiptoe past and find a **Breadcrumb Magnet** humming in an open vault drawer. No wonder the Duchess is so rich.', + reward: 'breadcrumbMagnet', + xpDelta: 8, + }, + ], + }, + { + id: 'vault_heist', + type: 'discovery', + location: 'breadcrumbVault', + narrative: + 'Deep in the Vault, you find a room labeled *"EMERGENCY RESERVES — DO NOT EAT."* The breadcrumbs here are enormous — the size of your head. One of them is glowing.\n\nA sign reads: *"Authorized personnel only. If you can read this, you\'re probably not authorized."*', + choices: [ + { + label: 'Take the glowing one', + outcome: + "You grab the glowing breadcrumb. It pulses with power — a **Bread of Fury** forged from the Duchess's personal reserve. The alarm doesn't go off. Probably.", + reward: 'damageBoost', + breadcrumbDelta: -7, + }, + { + label: 'Read the fine print', + outcome: + 'You lean in to read the microscopic text: *"Any duck reading this is hereby entitled to one (1) complimentary beverage."* A **Pond Water Espresso** materializes.', + reward: 'energyDrink', + breadcrumbDelta: 2, + }, + ], + }, + { + id: 'vault_accountant', + type: 'lore', + location: 'breadcrumbVault', + narrative: + 'A ghostly duck in spectacles hovers over a ledger, muttering numbers. *"The Duchess\'s fortune... thirty-seven million crumbs... adjusted for inflation... carry the two..."*\n\nIt notices you. *"Oh! A living duck! Quick — can you verify this entry? I\'ve been dead for two hundred years and I\'ve lost count."*', + choices: [ + { + label: 'Help with the math', + outcome: + 'You spend an hour doing ghost-accounting. The spectral duck weeps with joy. *"FINALLY! The books balance! Take this — I don\'t need it anymore."* A **Shield Token** appears.', + reward: 'shieldToken', + }, + { + label: 'Tell him the Duchess went bankrupt', + outcome: + 'The ghost screams, drops everything, and vanishes through the floor. A **Bread Poultice** falls from the scattered papers. That was mean, but effective.', + reward: 'hpPotion', + }, + ], + }, + { + id: 'vault_safe', + type: 'discovery', + location: 'breadcrumbVault', + narrative: + 'Behind a golden loaf sculpture, you discover a hidden safe. The combination lock has breadcrumb-shaped dials. A faded note reads: *"Combination: The Duchess\'s birthday. (No one knows the Duchess\'s birthday.)"*', + choices: [ + { + label: 'Guess randomly', + outcome: + 'You spin the dials. Click. Click. CLICK. It opens! Inside: a pristine **Bread of Fury** and a note: *"Happy birthday to me. —D.F."* You got lucky.', + reward: 'damageBoost', + }, + { + label: 'Pry it open with brute force', + outcome: + 'You wedge your beak in and HEAVE. The safe pops open with a satisfying crunch. Inside: a **Pond Water Espresso** in a diamond-encrusted thermos. The Duchess lives well.', + reward: 'energyDrink', + }, + { + label: 'Ask the ghost accountant', + outcome: + 'The ghost materializes, adjusts his spectacles. *"March 14th. Breadcrumb Day. Obviously."* The safe opens. A **Shield Token** glows inside.', + reward: 'shieldToken', + }, + ], + }, +] + +// --- Great Nest Encounters --- + +const NEST_ENCOUNTERS: EncounterDefinition[] = [ + { + id: 'nest_echoes', + type: 'lore', + location: 'greatNest', + narrative: + 'Golden feathers drift like snow. As you wade deeper into **The Great Nest**, voices echo from the woven walls — every duck who ever lived, whispering fragments of memory.\n\nOne voice grows louder: *"Find it. The feather that started everything. It\'s here. It was always here."*', + choices: [ + { + label: 'Follow the voice', + outcome: + "You push through curtains of ancient feathers until you find it — a single golden plume embedded in the nest's heart. Touching it fills you with power. A **Bread of Fury** crystallizes in your wing.", + reward: 'damageBoost', + }, + { + label: 'Listen to all the voices', + outcome: + 'You stand still and let the chorus wash over you. Names, battles, breadcrumbs won and lost. The knowledge heals something deep inside. A **Bread Poultice** appears, warm and ancient.', + reward: 'hpPotion', + }, + ], + }, + { + id: 'nest_guardian_spirit', + type: 'npc', + location: 'greatNest', + narrative: + 'A translucent duck materializes before you — enormous, regal, and slightly annoyed. *"I am the Guardian of the Nest. Every century, some fool wanders in here looking for power."*\n\nIt sighs. *"Fine. Prove you deserve to be here."*', + choices: [ + { + label: 'Challenge the Guardian', + outcome: + 'You square up. The Guardian charges — and passes right through you. It\'s a ghost. *"Oh. Right. I\'m dead."* It shrugs and fades away. No loot — but the courage it took leaves a mark on your spirit.', + xpDelta: 20, + }, + { + label: 'Offer a breadcrumb in tribute', + outcome: + "The Guardian's eyes widen. *\"A breadcrumb?! Do you know how long it's been since I've eaten?! ...Two thousand years. The answer is two thousand years.\"* It grants you a **Pond Water Espresso** in gratitude.", + reward: 'energyDrink', + breadcrumbDelta: 4, + }, + { + label: 'Tell a joke', + outcome: + '"Why did the duck cross the Nest? To get to the other side quest." The Guardian stares. Then HOWLS with laughter. *"THAT\'S TERRIBLE! I love it!"* A **Bread of Fury** materializes.', + reward: 'damageBoost', + xpDelta: 10, + }, + ], + }, + { + id: 'nest_egg_chamber', + type: 'discovery', + location: 'greatNest', + narrative: + 'You enter a chamber lined with enormous eggs — six of them, each a different color. One pedestal stands empty. A plaque reads: *"Six eggs hatched. One did not. Six stories ended. One continues."*\n\nThe empty pedestal hums with residual power.', + choices: [ + { + label: 'Touch the empty pedestal', + outcome: + 'Power surges through you. For a moment, you see the Seventh Egg — golden, pulsing, alive. The vision fades but leaves behind a **Shield Token** forged from ancient shell fragments.', + reward: 'shieldToken', + breadcrumbDelta: 3, + }, + { + label: 'Examine the hatched eggs', + outcome: + 'Each shell contains a fragment of history. Inside one, wrapped in ancient feathers: a **Bread Poultice** from before the First Quacktament. It still works.', + reward: 'hpPotion', + }, + { + label: 'Search for hidden passages', + outcome: + 'Behind the sixth egg, a narrow tunnel leads to a hidden alcove. Inside: a **Quack Grenade** forged from golden eggshell. It hums with primal destructive energy — the kind that ended the Second Age.', + reward: 'quackGrenade', + breadcrumbDelta: -5, + }, + ], + }, + { + id: 'nest_ancient_mural', + type: 'lore', + location: 'greatNest', + narrative: + 'A massive mural stretches across the Nest wall, depicting **The Great Mallard** mid-sneeze — the moment the Quackverse was born. In the painting, seven eggs orbit the Mallard. Six glow. One is dark.\n\nBelow: *"When the Seventh awakens, the Quackverse will remember what it forgot."*', + choices: [ + { + label: 'Touch the dark egg in the mural', + outcome: + 'The painted egg glows briefly. A **Pond Water Espresso** falls from the wall — condensed from pure primordial Quackverse energy. It tastes like eternity.', + reward: 'energyDrink', + }, + { + label: "Trace the Mallard's outline", + outcome: + "Your wing follows the divine sneeze. The wall trembles and a shard of the mural's surface peels away — it's a **Mirror Shard**, reflecting not your face but the face of every duck who ever stood here. Ancient protection magic.", + reward: 'mirrorShard', + }, + ], + }, +] + +// --- Quest-Specific Encounters --- + +const QUEST_ENCOUNTERS: EncounterDefinition[] = [ + { + id: 'quest_riddle_1', + type: 'npc', + location: 'quackatoa', + questTrigger: { questId: 'bigmouth_riddles', stepId: 'riddles' }, + narrative: + '**Big Mouth McGee** erupts from the lava, steam hissing from his beak. *"RIDDLE THE FIRST!"*\n\n' + + 'He clears his throat dramatically.\n\n' + + '*"What has feathers but can\'t fly, swims but has no fins?"*', + choices: [ + { + label: 'A rubber duck!', + outcome: + '*McGee\'s jaw drops.* "CORRECT! A RUBBER DUCK! Feathers painted on, no fins, ' + + 'just vibes and buoyancy!" He tosses you a glowing breadcrumb. *"One down, two to go!"*', + xpDelta: 15, + }, + { + label: 'A penguin?', + outcome: + '*McGee slaps the lava in frustration.* "A PENGUIN?! Penguins have fins! ' + + 'Those little flipper things! DISQUALIFIED!" He dives under and resurfaces. ' + + '*"Try again next time, featherbrain!"*', + breadcrumbDelta: -5, + }, + { + label: 'McGee himself?', + outcome: + '*McGee pauses. His eyes narrow. Then he HOWLS with laughter.* ' + + '"ME?! I can FLY! ...Theoretically! The lava makes it complicated!" ' + + 'He wipes a tear and tosses you a small reward. *"Points for cheek."*', + xpDelta: 8, + }, + ], + }, + { + id: 'quest_riddle_2', + type: 'npc', + location: 'quackatoa', + questTrigger: { questId: 'bigmouth_riddles', stepId: 'riddles' }, + narrative: + '**Big Mouth McGee** rises again, volcanic bubbles popping around his enormous beak.\n\n' + + '*"RIDDLE THE SECOND! Pay attention!"*\n\n' + + '*"What falls but never breaks, and breaks but never falls?"*', + choices: [ + { + label: 'Night and day!', + outcome: + '*McGee clutches his chest.* "NIGHT FALLS! DAY BREAKS! ' + + 'You absolute GENIUS of a duck!" Lava geysers erupt in celebration. ' + + '*"Two down! The final riddle awaits!"*', + xpDelta: 15, + }, + { + label: 'Waterfall and dawn?', + outcome: + '*McGee tilts his head.* "A waterfall DOES fall... and dawn DOES break... ' + + 'but that\'s TWO THINGS not two HALVES of the same answer!" ' + + 'He begrudgingly nods. *"Close enough for a pelican\'s standards."*', + xpDelta: 10, + }, + { + label: 'My spirit after this riddle?', + outcome: + '*McGee stares at you. A single volcanic tear rolls down his beak.* ' + + '"That\'s... that\'s the most relatable thing anyone has ever said to me." ' + + 'He hands you a warm breadcrumb. *"I feel SEEN."*', + xpDelta: 8, + reward: 'hpPotion', + }, + ], + }, + { + id: 'quest_riddle_3', + type: 'npc', + location: 'quackatoa', + questTrigger: { questId: 'bigmouth_riddles', stepId: 'riddles' }, + narrative: + '**Big Mouth McGee** surfaces one final time, wreathed in volcanic smoke.\n\n' + + '*"THE FINAL RIDDLE! This one separates the ducks from the ducklings!"*\n\n' + + '*"I speak without a mouth and hear without ears. What am I?"*', + choices: [ + { + label: 'An echo!', + outcome: + "*McGee's beak falls open. He looks genuinely impressed.* " + + '"An ECHO! Speaks without a mouth! Hears without ears! ' + + 'You... you actually solved all three." He reaches into his pouch ' + + 'and produces a glowing feather token. *"You\'ve EARNED this."*', + xpDelta: 20, + reward: 'shieldToken', + }, + { + label: "Gerald's HONK!", + outcome: + '*McGee considers this.* "Gerald\'s honk DOES echo through the ' + + 'thunderclouds... and it DOES seem to hear everything..." He nods slowly. ' + + '*"Not the answer I was looking for, but I respect the lateral thinking."*', + xpDelta: 12, + }, + { + label: 'This bot?', + outcome: + '*McGee squints.* "What\'s a bot?" He looks around nervously. ' + + '*"Are you having a PHILOSOPHICAL CRISIS in my volcano?!"* ' + + 'He tosses you a breadcrumb to snap you out of it.', + xpDelta: 5, + reward: 'energyDrink', + }, + ], + }, + { + id: 'quest_harold_chess', + type: 'npc', + location: 'parkBench', + questTrigger: { questId: 'harold_chess', stepId: 'chess_play' }, + narrative: + '**Harold** sits across from you. The chessboard is set. The pieces are breadcrumbs — ' + + 'white sourdough vs dark rye. Harold has been waiting for this moment. ' + + 'Possibly for decades.\n\n' + + 'The park is silent. A pigeon watches from a nearby tree. ' + + 'Even the wind holds its breath.\n\n' + + '*It is your move.*', + choices: [ + { + label: "Open with the King's Crumbit", + outcome: + 'You advance your king-side crumb two squares. Harold studies the board for eleven minutes. ' + + 'Then he moves a rye pawn one square forward. Twenty-three moves later, Harold wins. ' + + 'He always wins. But his eyes say something new: *respect.*\n\n' + + 'He slides a bread poultice across the board. For the journey ahead.', + reward: 'hpPotion', + xpDelta: 15, + }, + { + label: "Mirror Harold's opening move", + outcome: + "You copy Harold's pawn push exactly. He raises an eyebrow — " + + 'the most emotion he has displayed in thirty years. The game becomes a mirror match. ' + + 'Move for move, crumb for crumb. Harold wins on move forty-one with a technique ' + + "that shouldn't be possible with breadcrumbs.\n\n" + + 'He nods once. A golden feather drifts from his coat.', + reward: 'shieldToken', + xpDelta: 15, + }, + { + label: 'Knock the board over and run', + outcome: + 'Breadcrumbs scatter across the bench. Harold freezes. The pigeon gasps. ' + + 'You bolt three steps before guilt stops you.\n\n' + + 'When you turn back, Harold is calmly resetting the pieces. ' + + 'He does not look up. But on the bench where you sat: an energy drink ' + + 'and a note that reads simply: *"Coward. But an honest one."*', + reward: 'energyDrink', + xpDelta: 10, + }, + ], + }, + { + id: 'quest_thundercloud_manuscript', + type: 'discovery', + location: 'highway', + questTrigger: { questId: 'frozen_prophecy', stepId: 'manuscript' }, + narrative: + 'Gerald banks hard into a thundercloud. Lightning crackles. And there — ' + + 'etched into the underside of a massive goose wing-shaped cloud formation — ' + + 'you see it: **ancient text**, glowing faintly with residual Quackverse magic.\n\n' + + 'The **Thundercloud Manuscript**. Frostbeak was right. The migration routes ' + + 'hold secrets older than the Coliseum itself.\n\n' + + 'The symbols shimmer and shift. You need to capture them before the storm passes.', + choices: [ + { + label: 'Read it carefully', + outcome: + 'You hover in the storm, eyes tracing every glyph. The ancient Old Quack script ' + + 'burns itself into your memory — migration routes, star patterns, and a single ' + + 'phrase repeated seven times: *"The egg that waits."*\n\n' + + 'The scroll materializes in your wing, warm despite the freezing rain. ' + + 'Gerald honks approvingly. *Knowledge is its own reward — but you also find a potion.*', + reward: 'hpPotion', + xpDelta: 20, + }, + { + label: 'Trace the symbols', + outcome: + 'You extend your wing and trace each symbol in the air. Where your feathers pass, ' + + 'the glyphs solidify into golden light. The manuscript assembles itself ' + + 'piece by piece — a map of the ancient migration, leading to something called ' + + '*"The Great Nest."*\n\n' + + 'A golden feather crystallizes from the storm. The manuscript chose you.', + reward: 'shieldToken', + xpDelta: 20, + }, + { + label: 'Take a rubbing with breadcrumbs', + outcome: + 'You press a breadcrumb against the cloud surface and rub. Somehow, impossibly, ' + + 'the ancient text transfers onto the bread. You now hold the only breadcrumb ' + + 'in existence that is also a historical document.\n\n' + + 'Gerald stares at the bread-manuscript. He honks twice. That means *"impressive."*', + reward: 'damageBoost', + xpDelta: 20, + }, + ], + }, +] + +// --- Encounter Engine --- + +const ALL_ENCOUNTERS = [ + ...NPC_ENCOUNTERS, + ...DISCOVERY_ENCOUNTERS, + ...LORE_ENCOUNTERS, + ...VAULT_ENCOUNTERS, + ...NEST_ENCOUNTERS, + ...QUEST_ENCOUNTERS, +] + +export const rollEncounter = ( + locationId: LocationId, + completedIds: string[], + activeQuestSteps?: { questId: string; stepId: string }[] +): EncounterDefinition | undefined => { + // 0. If player has active quest steps, check for matching quest-specific encounters first + if (activeQuestSteps && activeQuestSteps.length > 0) { + const questMatches = QUEST_ENCOUNTERS.filter( + (e) => + (e.location === locationId || e.location === 'any') && + e.questTrigger && + activeQuestSteps.some((qs) => qs.questId === e.questTrigger!.questId && qs.stepId === e.questTrigger!.stepId) && + !completedIds.includes(e.id) + ) + if (questMatches.length > 0) { + return pick(questMatches) + } + } + + // 1. Try fresh non-quest encounters at this location (including 'any' encounters) + const available = ALL_ENCOUNTERS.filter( + (e) => (e.location === locationId || e.location === 'any') && !completedIds.includes(e.id) && !e.questTrigger + ) + if (available.length > 0) { + return pick(available) + } + + // 2. Try fresh 'any'-location non-quest encounters only + const anyFresh = ALL_ENCOUNTERS.filter((e) => e.location === 'any' && !completedIds.includes(e.id) && !e.questTrigger) + if (anyFresh.length > 0) { + return pick(anyFresh) + } + + // 3. Recycle: allow replaying location-specific non-quest encounters (player already saw them) + const locationPool = ALL_ENCOUNTERS.filter( + (e) => (e.location === locationId || e.location === 'any') && !e.questTrigger + ) + if (locationPool.length > 0) { + return pick(locationPool) + } + + return undefined +} + +export const resolveEncounterChoice = ( + encounter: EncounterDefinition, + choiceIndex: number +): EncounterChoice | undefined => { + if (choiceIndex < 1 || choiceIndex > encounter.choices.length) { + return undefined + } + return encounter.choices[choiceIndex - 1] +} + +export const getEncounterById = (id: string): EncounterDefinition | undefined => { + if (id === TUTORIAL_ENCOUNTER.id) { + return TUTORIAL_ENCOUNTER + } + return ALL_ENCOUNTERS.find((e) => e.id === id) +} + +export const formatEncounter = (encounter: EncounterDefinition): string => { + const choiceList = encounter.choices.map((c, i) => `**${i + 1}.** ${c.label}`).join('\n') + return `${encounter.narrative}\n\n${choiceList}` +} diff --git a/bots/quack-norris/src/lib/items.ts b/bots/quack-norris/src/lib/items.ts new file mode 100644 index 00000000000..7e68a41a1ef --- /dev/null +++ b/bots/quack-norris/src/lib/items.ts @@ -0,0 +1,142 @@ +export type ItemType = + | 'hpPotion' + | 'energyDrink' + | 'shieldToken' + | 'damageBoost' + | 'mirrorShard' + | 'quackGrenade' + | 'breadcrumbMagnet' + | 'fogBomb' + +export type ItemDefinition = { + id: ItemType + name: string + emoji: string + description: string + effect: string +} + +export const ITEMS: Record = { + hpPotion: { + id: 'hpPotion', + name: 'Bread Poultice', + emoji: '🍞', + description: 'A medicinal bread compress soaked in ancient pond water. Restores 20 HP instantly.', + effect: '+20 HP', + }, + energyDrink: { + id: 'energyDrink', + name: 'Pond Water Espresso', + emoji: '☕', + description: 'Triple-distilled pond scum with a breadcrumb garnish. Restores 15 energy instantly.', + effect: '+15 Energy', + }, + shieldToken: { + id: 'shieldToken', + name: "Chuck's Feather Token", + emoji: '🪶', + description: 'A golden feather plucked from Chuck Norris himself. Absorbs 25 damage once.', + effect: 'Absorb next 25 dmg', + }, + damageBoost: { + id: 'damageBoost', + name: 'Bread of Fury', + emoji: '🔥', + description: 'Enchanted sourdough forged in the fires of Quackatoa. +10 dmg on next attack.', + effect: '+10 dmg next attack', + }, + mirrorShard: { + id: 'mirrorShard', + name: 'Mirror Shard', + emoji: '🪞', + description: "A fragment of the Duchess's legendary vanity mirror. Reflects the next incoming attack.", + effect: 'Reflect next incoming attack', + }, + quackGrenade: { + id: 'quackGrenade', + name: 'Quack Grenade', + emoji: '💣', + description: 'An unstable breadcrumb device. Deals 15 dmg to all enemies.', + effect: '15 AoE dmg to all foes', + }, + breadcrumbMagnet: { + id: 'breadcrumbMagnet', + name: 'Breadcrumb Magnet', + emoji: '🧲', + description: 'Attracts stray breadcrumbs. Doubles breadcrumb rewards from your next encounter.', + effect: '2x breadcrumbs next encounter', + }, + fogBomb: { + id: 'fogBomb', + name: 'Fog Bomb', + emoji: '🌫️', + description: 'A vial of concentrated pond mist. All attacks against you miss for 1 round.', + effect: 'Dodge all attacks 1 round', + }, +} + +export const MAX_INVENTORY_SIZE = 6 + +export const formatInventory = ( + inventory: Array<{ itemId: string; name: string; type: ItemType; quantity: number }> +): string => { + if (inventory.length === 0) { + return '*Your inventory is empty. Explore the Quackverse to find items!*' + } + return inventory + .map((item) => { + const def = ITEMS[item.type] + return `${def.emoji} **${def.name}** x${item.quantity} — ${def.effect}` + }) + .join('\n') +} + +export const addItemToInventory = ( + inventory: Array<{ itemId: string; name: string; type: ItemType; quantity: number }>, + itemType: ItemType +): boolean => { + const def = ITEMS[itemType] + const existing = inventory.find((i) => i.type === itemType) + if (existing) { + existing.quantity += 1 + return true + } + if (inventory.length >= MAX_INVENTORY_SIZE) { + return false + } + inventory.push({ itemId: itemType, name: def.name, type: itemType, quantity: 1 }) + return true +} + +export const resolveItemName = (name: string): ItemType | undefined => { + const lower = name.toLowerCase() + const aliases: Record = { + hp: 'hpPotion', + potion: 'hpPotion', + bread: 'hpPotion', + hppotion: 'hpPotion', + energy: 'energyDrink', + espresso: 'energyDrink', + coffee: 'energyDrink', + energydrink: 'energyDrink', + shield: 'shieldToken', + feather: 'shieldToken', + token: 'shieldToken', + shieldtoken: 'shieldToken', + damage: 'damageBoost', + fury: 'damageBoost', + damageboost: 'damageBoost', + mirror: 'mirrorShard', + shard: 'mirrorShard', + mirrorshard: 'mirrorShard', + grenade: 'quackGrenade', + quackgrenade: 'quackGrenade', + bomb: 'quackGrenade', + magnet: 'breadcrumbMagnet', + breadcrumbmagnet: 'breadcrumbMagnet', + fog: 'fogBomb', + fogbomb: 'fogBomb', + mist: 'fogBomb', + } + return aliases[lower] +} diff --git a/bots/quack-norris/src/lib/locations.ts b/bots/quack-norris/src/lib/locations.ts new file mode 100644 index 00000000000..67ed62a7a2d --- /dev/null +++ b/bots/quack-norris/src/lib/locations.ts @@ -0,0 +1,136 @@ +export type LocationId = + | 'coliseum' + | 'puddle' + | 'highway' + | 'quackatoa' + | 'parkBench' + | 'frozenPond' + | 'breadcrumbVault' + | 'greatNest' + +export type LocationDefinition = { + id: LocationId + name: string + emoji: string + description: string + arrivalText: string + npcs: string[] + encounterTags: string[] + requiresQuestId?: string + requiresLevel?: number +} + +export const LOCATIONS: Record = { + coliseum: { + id: 'coliseum', + name: 'The Breadcrumb Coliseum', + emoji: '🏟️', + description: + 'The grand arena at the heart of The Pond Eternal. Built from petrified breadcrumbs and ancient quacking magic.', + arrivalText: + 'You arrive at the **Breadcrumb Coliseum**. The crowd roars as ten thousand ducks shuffle in their seats. The scent of stale bread fills the air. Chuck Norris watches from his golden nest above.', + npcs: ['duchess', 'attenbird'], + encounterTags: ['combat_lore', 'item', 'npc_duchess'], + }, + puddle: { + id: 'puddle', + name: 'The Puddle of Doom', + emoji: '💧', + description: 'A suburban puddle of terrifying mundanity. A Honda Civic is parked nearby.', + arrivalText: + 'You waddle up to **The Puddle of Doom**. It looks... ordinary. Too ordinary. A Honda Civic idles in the parking lot. A shopping cart rattles in the wind. Something is deeply wrong here.', + npcs: ['chad'], + encounterTags: ['suburban_horror', 'item', 'npc_chad'], + }, + highway: { + id: 'highway', + name: 'The Migration Highway', + emoji: '🦅', + description: 'A mid-air route atop Gerald the flying goose. Hold on tight.', + arrivalText: + 'You hop aboard **Gerald**, the legendary flying goose. The wind whips through your feathers as the Migration Highway stretches endlessly ahead. Gerald honks once. He does not elaborate.', + npcs: ['gerald'], + encounterTags: ['aerial', 'item', 'npc_gerald'], + }, + quackatoa: { + id: 'quackatoa', + name: 'Lake Quackatoa', + emoji: '🌋', + description: 'A volcanic lake of obsidian cliffs and steam vents. The water is uncomfortably warm.', + arrivalText: + 'The ground trembles as you approach **Lake Quackatoa**. Obsidian cliffs tower overhead. Steam vents hiss like angry kettles. Lava bubbles at the edges. This is either very dangerous or a very aggressive hot tub.', + npcs: ['bigmouth'], + encounterTags: ['volcanic', 'item', 'npc_bigmouth'], + }, + parkBench: { + id: 'parkBench', + name: 'The Park Bench Thunderdome', + emoji: '🪑', + description: 'A park bench where Harold the old man sits. He throws bread sometimes.', + arrivalText: + 'You arrive at **The Park Bench Thunderdome**. Harold, the old man, sits perfectly still on his bench. He watches you with the intensity of someone who has seen too much. A pigeon lands nearby. Harold does not blink.', + npcs: ['harold'], + encounterTags: ['park', 'item', 'npc_harold'], + }, + frozenPond: { + id: 'frozenPond', + name: 'The Frozen Pond', + emoji: '🧊', + description: 'A treacherous sheet of ice. Everything is slippery. Nothing is safe.', + arrivalText: + 'You step onto **The Frozen Pond** and immediately regret it. Your webbed feet skid in every direction. The ice is mirror-smooth and deeply unforgiving. Somewhere, a duck is screaming. It might be you.', + npcs: ['trenchbill', 'frostbeak'], + encounterTags: ['frozen', 'item', 'npc_frostbeak'], + }, + breadcrumbVault: { + id: 'breadcrumbVault', + name: 'The Breadcrumb Vault', + emoji: '🏦', + description: 'A fortified vault beneath the Coliseum where the wealthiest ducks hoard their crumbs.', + arrivalText: + "You descend into **The Breadcrumb Vault**. Enormous iron doors groan open. Inside: mountains of preserved breadcrumbs, golden loaf sculptures, and the faint sound of an accountant weeping with joy. The Duchess's seal glows on the wall. You belong here now.", + npcs: [], + encounterTags: ['vault', 'item', 'rare'], + requiresQuestId: 'duchess_favor', + }, + greatNest: { + id: 'greatNest', + name: 'The Great Nest', + emoji: '🪺', + description: 'The mythical nest at the heart of the Quackverse. Ancient, vast, and humming with power.', + arrivalText: + 'You pass through the Frozen Gate into **The Great Nest**. It stretches beyond sight — woven from golden feathers and petrified reeds older than memory. The air hums with something primal. Every duck who ever lived left a feather here. Somewhere deep within, something waits.', + npcs: [], + encounterTags: ['nest', 'legendary', 'item'], + requiresQuestId: 'frozen_prophecy', + requiresLevel: 7, + }, +} + +export const ALL_LOCATION_IDS = Object.keys(LOCATIONS) as LocationId[] + +/** Toll rates for gated locations (first visit free, subsequent visits cost breadcrumbs) */ +export const TOLL_RATES: Partial> = { + breadcrumbVault: { cost: 10, label: '10 🍞 toll' }, + greatNest: { cost: 15, label: '15 🍞 toll' }, +} + +export const formatLocationList = (currentLocation: LocationId, unlockedLocations?: string[]): string => { + return Object.values(LOCATIONS) + .map((loc, i) => { + const current = loc.id === currentLocation ? ' *(you are here)*' : '' + const locked = unlockedLocations && !unlockedLocations.includes(loc.id) ? ' 🔒' : '' + const toll = TOLL_RATES[loc.id] + const tollTag = toll && !locked ? ` *(${toll.label})*` : '' + return `**${i + 1}.** ${loc.emoji} ${loc.name}${current}${locked}${tollTag}` + }) + .join('\n') +} + +export const getLocationByIndex = (index: number): LocationDefinition | undefined => { + const ids = ALL_LOCATION_IDS + if (index < 1 || index > ids.length) { + return undefined + } + return LOCATIONS[ids[index - 1]!] +} diff --git a/bots/quack-norris/src/lib/narration.ts b/bots/quack-norris/src/lib/narration.ts new file mode 100644 index 00000000000..0e855f41bbf --- /dev/null +++ b/bots/quack-norris/src/lib/narration.ts @@ -0,0 +1,414 @@ +import { DUCK_CLASSES } from './classes' +import { renderHpBar, renderEnergyBar } from './combat' +import type { DuckClass, Player } from './types' + +const pick = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)]! + +// --- Light Attack Templates --- +const LIGHT_ATTACK_TEMPLATES = [ + "{attacker} delivers a swift wing-slap to {target}'s face! {damage} dmg! The audacity!", + '{attacker} pecks {target} with surgical precision. {damage} dmg. It looked like it stung.', + '{attacker} waddles up and casually smacks {target} for {damage} dmg. Disrespectful.', + 'A quick jab from {attacker}! {target} takes {damage} dmg and looks personally offended.', + '{attacker} does the classic drive-by wing-buffet! {target} eats {damage} dmg for lunch!', + '{attacker} feints left, feints right, then just... bonks {target}. {damage} dmg. Simple but effective.', + 'With the grace of a swan and the mercy of a goose, {attacker} taps {target} for {damage} dmg.', + '{attacker} throws a rapid-fire peck combo! {target} absorbs {damage} dmg of pure beak fury!', + 'QUACK! {attacker} lands a sneaky tail-whip on {target}! {damage} dmg! Where did that come from?!', + '{attacker} launches a bread-speed strike! {target} takes {damage} dmg before they can even blink!', +] + +const LIGHT_ATTACK_LOW_DMG_TEMPLATES = [ + '{attacker} barely grazes {target} for {damage} dmg. That was more of a suggestion than an attack.', + "{attacker} hits {target} for {damage} dmg. {target} isn't even sure they were hit. Was that a breeze?", + 'A glancing blow from {attacker}! {target} takes {damage} dmg and looks more confused than hurt.', + '{attacker} connects with {target} for {damage} dmg. Somewhere, Chuck Norris sighs in disappointment.', + "{attacker} taps {target} for {damage} dmg. It's like being attacked by a strong opinion.", +] + +// --- Heavy Attack Templates --- +const HEAVY_ATTACK_TEMPLATES = [ + '{attacker} winds up a DEVASTATING wing slam on {target}! {damage} dmg! The pond trembles!', + '{attacker} unleashes the Quack Attack on {target}! A thunderous {damage} dmg! Bread crumbs fly everywhere!', + 'WHAM! {attacker} hits {target} with a full-body cannonball! {damage} dmg! Someone call a vet!', + '{attacker} channels the spirit of Chuck Norris and OBLITERATES {target} for {damage} dmg!', + 'The crowd gasps as {attacker} delivers a flying drop-kick to {target}! {damage} dmg! Absolutely feral!', + '{attacker} does the forbidden Spinning Beak Tornado on {target}! {damage} dmg! That move was banned in three ponds!', + 'With righteous fury, {attacker} brings down the Hammer of Quack on {target}! {damage} dmg! The arena shakes!', + "{attacker} goes FULL GOOSE on {target}! {damage} dmg! That's not even legal in most waterways!", + "A CRITICAL WADDLE from {attacker}! {target} takes a staggering {damage} dmg! The physics don't even make sense!", + '{attacker} summons the ancient art of Wing-Fu and demolishes {target} for {damage} dmg! Sensational!', +] + +// --- Critical Hit Templates --- +const CRIT_TEMPLATES = [ + "CRITICAL HIT! {attacker} just discovered the meaning of maximum damage on {target}'s face! {damage} dmg!", + "OH QUACK! That's a CRIT! {attacker} channels pure chaos into {target} for {damage} dmg! Chuck Norris nods from above!", + 'DEVASTATING! {attacker} rolls a natural 20 on {target}! {damage} dmg! The arena falls silent, then erupts!', + 'A PERFECT strike from {attacker}! {target} takes {damage} dmg and briefly sees the face of The Great Mallard!', + 'QUACK-A-DOODLE-DOOM! {attacker} lands the hit of a lifetime on {target}! {damage} dmg! Write that one down!', +] + +// --- Block Templates --- +const BLOCK_SUCCESS_TEMPLATES = [ + "{attacker} swings with everything they've got, but {target} saw it coming! BLOCKED! Not today!", + "CLANG! {attacker}'s heavy attack meets {target}'s iron defense! 0 dmg! The crowd goes wild!", + '{attacker} launches a devastating strike but {target} blocks it like they read the script! DENIED!', + '{target} catches {attacker}\'s heavy blow with pure defensive instinct! "Nice try," they quack.', + "The force of {attacker}'s attack sends shockwaves through the arena, but {target} doesn't budge! BLOCKED!", + '{attacker} winds up the big one... and {target} yawns while deflecting it. Embarrassing.', + "BONK goes {attacker}'s attack right off {target}'s shield! The only thing damaged here is {attacker}'s ego.", + "{target} blocks {attacker}'s heavy attack so hard it creates a small sonic boom. The breadcrumbs scatter!", +] + +const BLOCK_NO_ATTACK_TEMPLATES = [ + '{player} hunkers down behind their wings. Nothing comes. Awkward.', + "{player} braces for impact... and waits... and waits. The attack never comes. That's 60 seconds they'll never get back.", + '{player} assumes a perfect defensive stance. Nobody attacks them. They feel weirdly disappointed.', + "{player} blocks absolutely nothing. It's like bringing an umbrella to a drought.", + '{player} crouches defensively, eyes darting everywhere. Not a single attack. They stand back up, dusting off imaginary dust.', + '{player} spent the entire round blocking. The ghosts they were fighting put up a good match, though.', +] + +// --- Block Partial Templates (light attack vs block = 50% reduction) --- +const BLOCK_PARTIAL_TEMPLATES = [ + '{attacker} slips a quick strike past {target}\'s guard! Partially blocked — {damage} dmg! "That still stings!"', + "{target}'s block catches most of {attacker}'s light attack, but {damage} dmg gets through! A glancing blow!", + "DEFLECTED! {target} braces against {attacker}'s quick strike — only {damage} dmg lands. The block holds!", + '{attacker} pecks at {target}\'s defenses! The block absorbs half the blow — {damage} dmg! "Is that all you\'ve got?"', + "{target}'s shield arm strains as {attacker}'s light attack pushes through for {damage} dmg! Blocked, but felt.", + 'A chip shot from {attacker}! {target} blocks the worst of it but takes {damage} dmg through their guard. Resourceful!', +] + +// --- Dodge Templates --- +const DODGE_TEMPLATES = [ + "{target} ducks! (Yes, that pun was intentional.) {attacker}'s attack whiffs completely!", + 'DODGED! {target} moves like liquid! {attacker} hits nothing but air and regret!', + "{target} sidesteps with impossible grace! {attacker}'s {attackType} attack misses entirely!", + "Smoke and feathers! {target} vanishes from the path of {attacker}'s attack! MISSED!", + '{attacker} swings at {target} but {target} is already somewhere else entirely! How?!', + "{target} bends backwards like they just downloaded the Matrix! {attacker}'s {attackType} attack sails overhead!", + 'Now you see {target}, now you — wait, where did they go?! {attacker} strikes a ghost! A GHOST DUCK!', + "{target} does a perfect backflip over {attacker}'s {attackType} attack! The crowd loses their minds! Style points!", + 'The fog swallows {target} whole! {attacker} swings wildly at nothing! The mist laughs!', +] + +// --- Elimination Templates --- +const ELIMINATION_TEMPLATES = [ + '{player} collapses in a dramatic heap of feathers! ELIMINATED! Pour one out for the fallen fowl!', + 'AND {player} IS DOWN! The crowd hurls bread in tribute! A warrior falls! A legend begins!', + "{player} has been sent to the Great Pond in the Sky! (They'll respawn next game, don't worry.)", + "That's all she quacked! {player} has been ELIMINATED! Their feathers scatter in the wind dramatically!", + "{player} falls! As they hit the ground, a single golden feather drifts down from Chuck Norris's nest. Respect.", + 'ELIMINATED! {player} waddles off into the sunset... of defeat! Their journey ends here, but what a ride!', + '{player} has left the mortal pond! The breadcrumbs of destiny have spoken! ELIMINATED!', + 'Down goes {player}! DOWN GOES {player}! The arena medics rush in with emergency bread!', +] + +const FIRST_ELIMINATION_TEMPLATES = [ + '**FIRST BLOOD!** The arena claims its first victim! {player} has fallen! The Quacktament has truly begun!', + '**AND SO IT BEGINS!** {player} is the first to fall! Let their sacrifice remind us all: this pond takes no prisoners!', + '**ONE DOWN!** {player} has been sent to the spectator seats the hard way! The field narrows!', +] + +const FINAL_TWO_TEMPLATES = [ + '**THE FINAL TWO!** {eliminated} falls, leaving only {player1} and {player2}! THIS IS IT! THE ULTIMATE SHOWDOWN!', + '**DOWN TO THE WIRE!** {eliminated} is gone! {player1} vs {player2}! ONE FIGHT! ONE WINNER! ONE BREADCRUMB TO RULE THEM ALL!', + '**SUDDEN DEATH INCOMING!** {eliminated} exits stage left, and we have our FINAL DUEL! {player1} faces {player2}!', +] + +// --- Rest Templates --- +const REST_TEMPLATES = [ + '💤 {player} takes a breather, recovering energy. Smart... or cowardly? The crowd debates hotly. A pigeon is removed from the gallery.', + '💤 {player} sits down in the middle of the arena, produces a tiny thermos, and sips. The audacity of resting during a Quacktament. Bold. Reckless. Iconic.', + '💤 {player} closes their eyes and draws power from The Pond Eternal. The breadcrumbs around them levitate briefly. Energy restored.', + '💤 {player} pulls out a miniature breadcrumb snack and stress-eats it mid-combat. The crumbs fall like snow. Energy refueled. Dignity questionable.', + '💤 {player} folds their wings, tucks their head, and enters a micro-nap. The arena holds its breath. Three seconds later: eyes open. Recharged. Terrifying.', + '💤 {player} leans against the arena wall like they own the place. "Wake me when it gets interesting," they quack. Energy restored.', +] + +// --- Special Ability Templates --- +const SPECIAL_TEMPLATES: Record = { + mallardNorris: [ + '{caster} ROUNDHOUSE KICKS {target} with the fury of a thousand angry ducks! {damage} dmg! Chuck Norris sheds a single tear of pride!', + "IT'S THE ROUNDHOUSE! {caster} channels pure Norris energy into {target}! {damage} dmg! The arena floor CRACKS!", + "{caster} spins with the force of a duck tornado and DEMOLISHES {target}! {damage} dmg! That's gonna leave a mark!", + ], + quackdini: [ + '{caster} snaps their wing and a perfect Mirror Decoy appears! "Hit THIS," they quack with a smirk.', + 'POOF! {caster} conjures a shimmering decoy! The next attack will learn a painful lesson about trust!', + '{caster} performs the ancient art of Quackception! A Mirror Decoy stands ready to absorb and reflect!', + ], + sirQuacksALot: [ + '{caster} raises their wings to the sky and calls upon Holy Plumage! A Divine Shield descends upon {target}!', + 'By the power of The Great Mallard! {caster} bestows a Divine Shield on {target}! 30 dmg absorbed!', + "{caster}'s eyes glow golden as they channel divine energy into a protective shield for {target}!", + ], + drQuackenstein: [ + '{caster} cackles maniacally and hurls a PLAGUE BOMB! Toxic green mist engulfs the arena!', + 'PLAGUE BOMB! {caster} unleashes a concoction of questionable origin! Everyone is now slightly poisoned!', + '{caster} throws a bubbling vial that EXPLODES! Poison clouds everywhere! The breadcrumbs turn green!', + ], +} + +// --- Lore Intro (shown before class selection) --- +const LORE_INTRO_TEMPLATES = [ + '**Welcome to The Pond Eternal** — a pocket dimension where ducks achieved sentience, discovered violence, and decided it was hilarious. At its center stands the **Arena of Mallard Destiny**, built from petrified breadcrumbs. Chuck Norris watches from his golden nest above. Choose wisely.', + 'Long ago, **The Great Mallard** sneezed the universe into existence and laid seven primordial eggs. The seventh — the **Egg of Violence** — hatched Chuck Norris, the first Warrior Duck. He declared every duck must fight. And so began the **Eternal Quacktament**. Your turn.', + 'From the murky depths of **The Pond Eternal**, warriors gather at the **Breadcrumb Coliseum** — where crumbs become legends. Chuck Norris nods from his golden nest. The crowd of ten thousand ducks holds its breath. The Quacktament calls for new champions.', + 'In the Quackverse, all realities where ducks achieved sentience converge at one place: **The Pond Eternal**. At its heart, the **Arena of Mallard Destiny** awaits. Chuck Norris adjusts his tiny sunglasses. The breadcrumbs are fresh. It is time to choose your fighter.', +] + +export const narrateLoreIntro = (): string => { + return pick(LORE_INTRO_TEMPLATES) +} + +// --- Opening Ceremony --- +const OPENING_CEREMONY_TEMPLATES = [ + "**=== THE QUACKTAMENT BEGINS ===**\n\nThe trumpets blare! The breadcrumbs fly! The crowd of ten thousand ducks loses their collective mind!\n\nChuck Norris descends from his golden nest, gives a single approving nod, and the Arena floor trembles.\n\nFighters, the rules are simple: be the last duck standing. Or don't. We're not your mom.", + '**=== WELCOME TO THE ARENA OF MALLARD DESTINY ===**\n\nFrom the depths of The Pond Eternal, warriors have gathered! The Great Mallard watches from beyond! Chuck Norris adjusts his tiny sunglasses!\n\nThis is it. This is the moment. This is... *dramatic pause* ...QUACK-NORRIS!', + '**=== LET THE FEATHERS FLY ===**\n\nLadies, gentleducks, and whatever a goose is --\n\nWelcome to the only combat sport where the participants are adorable AND terrifying! The arena is set! The breadcrumbs are fresh! The violence is about to be UNREASONABLE!\n\nChuck Norris has blessed this match. May your quacks be mighty and your blocks be timely.', + '**=== THE POND ETERNAL TREMBLES ===**\n\nAnother day, another battle for the ages! Ducks have flown from every corner of the Quackverse to settle their differences the old-fashioned way: ORGANIZED VIOLENCE!\n\nChuck Norris cracks open a tiny soda in his golden nest. He is ready. Are you?', +] + +// --- Round Start Templates --- +const ROUND_START_EARLY = [ + '**-- Round {n} --**\nThe fighters circle each other like sharks in a very small pond. Breadcrumbs crunch underfoot. The crowd leans forward. Someone drops a pretzel. Nobody notices.', + "**-- Round {n} --**\nThe air crackles with barely-contained violence. Feathers drift like snow. Chuck Norris sips from a golden chalice. We're just getting started.", + '**-- Round {n} --**\nAnother round dawns on the Quacktament! The fighters flex their wings and narrow their eyes. The Arena Scribe dips their quill. History awaits.', + '**-- Round {n} --**\nThe breadcrumb dust settles. The fighters lock eyes across the arena. Somewhere in the stands, a duck drops their monocle. The tension is unbearable.', +] + +const ROUND_START_MID = [ + '**-- Round {n} --**\nThe battle rages on! Feathers carpet the arena floor like macabre confetti. The bread merchants have tripled their prices. War profiteering at its finest.', + "**-- Round {n} --**\nWe're deep in it now. The fighters' eyes tell stories of pain, determination, and a profound desire to not be the one who dies next. This is what legends are made of.", + '**-- Round {n} --**\nThe arena floor is more feather than breadcrumb at this point. A medic duck nervously sharpens a tongue depressor. Round {n} begins and even the wind holds its breath.', + '**-- Round {n} --**\nBlood, sweat, and breadcrumbs. The unholy trinity of the Quacktament. The fighters circle with the wary respect of opponents who have survived this long. Round {n}.', +] + +const ROUND_START_LATE = [ + '**-- Round {n} --**\nThis has gone LONG! The crowd is FERAL! Chuck Norris has leaned forward in his nest — and he NEVER leans forward! The Arena Scribe is running out of parchment! THIS IS HISTORY!', + "**-- Round {n} --**\nROUND {n}?! We haven't seen a fight go this deep since the Great Bread War of '09! These ducks are IMMORTAL! Or very, very stubborn! Either way, the crowd is getting their money's worth!", + '**-- Round {n} --**\nAt this point, the fighters are running on pure spite, adrenaline, and the fumes of breadcrumbs long since consumed. Round {n}! THE QUACKTAMENT DEMANDS MORE! THE QUACKTAMENT ALWAYS DEMANDS MORE!', + '**-- Round {n} --**\nThe sun has moved. The shadows have shifted. The breadcrumb vendors have closed shop and gone home. But the fighters? The fighters remain. Round {n}. This is personal now.', +] + +// --- Victory Templates --- +const VICTORY_TEMPLATES = [ + '**=== VICTORY! ===**\n\nThe arena ERUPTS! {winner} stands alone atop a mountain of breadcrumbs, wings spread, quacking triumphantly into the void!\n\nChuck Norris drops a golden feather from his nest. The highest honor.\n\n**{winner} is the CHAMPION OF THE QUACKTAMENT!**', + '**=== THE LAST DUCK STANDING ===**\n\nWhen the feathers settle and the dust clears, only one duck remains: **{winner}**!\n\nThey have fought. They have suffered. They have quacked in the face of adversity. And now? Now they hold the Golden Breadcrumb high.\n\nChuck Norris slow-claps from his nest. It sounds like thunder.', + '**=== CHAMPION CROWNED ===**\n\n*{winner}* has done the impossible, the improbable, the slightly ridiculous!\n\nAll hail **{winner}**, Champion of the Quacktament!', + '**=== GLORY! GLORY! QUACKELUJAH! ===**\n\nLet the record show: on this day, **{winner}** looked destiny in the eye, quacked at it, and WON!\n\nThe Golden Breadcrumb is theirs! The title is theirs!\n\nChuck Norris salutes. The universe quacks in approval.', +] + +// --- Draw Templates --- +const DRAW_TEMPLATES = [ + "**=== DRAW! ===**\n\nIn a twist nobody saw coming, ALL remaining fighters have fallen simultaneously! The Arena is silent. A tumbleweed made of feathers rolls by.\n\nChuck Norris shrugs. Even legends don't have answers for everything.\n\n**No winner today. But what a fight!**", + "**=== MUTUAL DESTRUCTION ===**\n\nThey came. They fought. They ALL fell. The breadcrumbs claim no champion today.\n\n**It's a draw! Everybody loses! Nobody wins! Somehow that's poetic!**", + "**=== TIME'S UP ===**\n\nThe sands of the hourglass have run dry and no champion has emerged!\n\nChuck Norris checks his watch and declares this one a DRAW.\n\n**Fight harder next time!**", +] + +// --- Sir David Attenbird Commentary --- +const LOW_HP_COMMENTARY = [ + '*Sir David Attenbird observes: "{player}, now critically wounded, draws upon reserves of strength they did not know they possessed. Or perhaps it is adrenaline. Or denial."*', + '*"{player} clings to consciousness with the tenacity of a duck clinging to the last breadcrumb in the bag. One suspects this cannot last much longer."*', + '*One notices {player} is looking rather... translucent. At {hp} HP, they are one strong breeze away from elimination.*', + '*The remarkable {player} continues to fight at {hp} HP! Most ducks would have surrendered, but this one appears to have chosen violence over self-preservation. Extraordinary.*', +] + +const FINAL_SHOWDOWN_COMMENTARY = [ + '*Sir David Attenbird whispers: "And here we are. Two ducks. One arena. The air itself seems to hold its breath. This is nature at its most... dramatic."*', + '*"The final two combatants lock eyes across the breadcrumb-strewn arena. {player1} vs {player2}. This is what we came for."*', + '*The entire Quackverse narrows to this single moment. {player1} ({hp1} HP) faces {player2} ({hp2} HP). The Golden Breadcrumb awaits.*', +] + +const UNDERDOG_COMMENTARY = [ + '*AGAINST ALL ODDS! {player}, battered and bruised at {hp} HP, just landed a MASSIVE hit! Chuck Norris STANDS UP in his nest!*', + '*Sir David Attenbird, visibly emotional: "The underdog rises! {player}, whom we all counted out, strikes back with fury!"*', + '*This is the comeback story we LIVE for! {player} at {hp} HP just went FULL LEGEND!*', +] + +const BLOODBATH_COMMENTARY = [ + '*Sir David Attenbird removes his monocle: "What... what just happened? {count} ducks fell in a single round! This is CARNAGE!"*', + '*A MASSACRE! {count} eliminations in one round! The arena is littered with defeated ducks and scattered feathers!*', +] + +const STALEMATE_COMMENTARY = [ + '*Sir David Attenbird coughs politely: "That round was... contemplative. No damage was dealt. Perhaps the fighters are engaged in psychological warfare."*', + '*Nothing happened. Literally nothing. Chuck Norris looks disappointed. The crowd throws stale bread.*', +] + +// --- Template Helpers --- + +const fill = (template: string, vars: Record): string => { + let result = template + for (const [key, value] of Object.entries(vars)) { + result = result.replaceAll(`{${key}}`, String(value)) + } + return result +} + +// --- Public API --- + +export const narrateLightAttack = (attacker: string, target: string, damage: number): string => { + const templates = damage <= 12 ? LIGHT_ATTACK_LOW_DMG_TEMPLATES : LIGHT_ATTACK_TEMPLATES + return fill(pick(templates), { attacker, target, damage }) +} + +export const narrateHeavyAttack = (attacker: string, target: string, damage: number): string => { + return fill(pick(HEAVY_ATTACK_TEMPLATES), { attacker, target, damage }) +} + +export const narrateCriticalHit = (attacker: string, target: string, damage: number): string => { + return fill(pick(CRIT_TEMPLATES), { attacker, target, damage }) +} + +export const narrateBlockSuccess = (attacker: string, target: string): string => { + return fill(pick(BLOCK_SUCCESS_TEMPLATES), { attacker, target }) +} + +export const narrateBlockNoAttack = (player: string): string => { + return fill(pick(BLOCK_NO_ATTACK_TEMPLATES), { player }) +} + +export const narrateBlockPartial = (attacker: string, target: string, damage: number): string => { + return fill(pick(BLOCK_PARTIAL_TEMPLATES), { attacker, target, damage }) +} + +export const narrateDodge = (attacker: string, target: string, attackType: string): string => { + return fill(pick(DODGE_TEMPLATES), { attacker, target, attackType }) +} + +export const narrateElimination = (player: string, isFirst: boolean, remainingPlayers?: { name: string }[]): string => { + if (isFirst) { + return fill(pick(FIRST_ELIMINATION_TEMPLATES), { player }) + } + + if (remainingPlayers && remainingPlayers.length === 2) { + return fill(pick(FINAL_TWO_TEMPLATES), { + eliminated: player, + player1: remainingPlayers[0]!.name, + player2: remainingPlayers[1]!.name, + }) + } + + return fill(pick(ELIMINATION_TEMPLATES), { player }) +} + +export const narrateRest = (player: string): string => { + return fill(pick(REST_TEMPLATES), { player }) +} + +export const narrateSpecial = (caster: string, target: string, duckClass: DuckClass, damage?: number): string => { + const templates = SPECIAL_TEMPLATES[duckClass] + return fill(pick(templates), { caster, target: target || 'themselves', damage: damage ?? 0 }) +} + +export const narrateOpeningCeremony = (): string => { + return pick(OPENING_CEREMONY_TEMPLATES) +} + +export const narrateRoundStart = (round: number): string => { + let templates: string[] + if (round <= 3) { + templates = ROUND_START_EARLY + } else if (round <= 7) { + templates = ROUND_START_MID + } else { + templates = ROUND_START_LATE + } + return fill(pick(templates), { n: round }) +} + +export const narrateVictory = (winner: string): string => { + return fill(pick(VICTORY_TEMPLATES), { winner }) +} + +export const narrateDraw = (): string => { + return pick(DRAW_TEMPLATES) +} + +// --- Contextual Commentary (Sir David Attenbird) --- + +export const generateCommentary = ( + players: Player[], + eliminationsThisRound: number, + totalDamageThisRound: number +): string | undefined => { + const alive = players.filter((p) => p.alive) + + // Bloodbath round + if (eliminationsThisRound >= 2) { + return fill(pick(BLOODBATH_COMMENTARY), { count: eliminationsThisRound }) + } + + // Final showdown + if (alive.length === 2) { + return fill(pick(FINAL_SHOWDOWN_COMMENTARY), { + player1: alive[0]!.name, + player2: alive[1]!.name, + hp1: alive[0]!.hp, + hp2: alive[1]!.hp, + }) + } + + // Low HP player + const lowHpPlayer = alive.find((p) => p.hp > 0 && p.hp <= p.maxHp * 0.25) + if (lowHpPlayer) { + return fill(pick(LOW_HP_COMMENTARY), { player: lowHpPlayer.name, hp: lowHpPlayer.hp }) + } + + // Stalemate + if (totalDamageThisRound === 0) { + return pick(STALEMATE_COMMENTARY) + } + + // Underdog (lowest HP player dealt damage) + const lowestHpAlive = alive.reduce((min, p) => (p.hp < min.hp ? p : min), alive[0]!) + if (lowestHpAlive.hp <= lowestHpAlive.maxHp * 0.3 && totalDamageThisRound > 20) { + return fill(pick(UNDERDOG_COMMENTARY), { player: lowestHpAlive.name, hp: lowestHpAlive.hp }) + } + + return undefined +} + +// --- Player Status Rendering --- + +export const renderPlayerStatus = (player: Player): string => { + const classDef = player.duckClass ? DUCK_CLASSES[player.duckClass] : undefined + const classTag = classDef ? ` ${classDef.emoji} ${classDef.name}` : '' + const hpBar = renderHpBar(player.hp, player.maxHp) + const energyBar = renderEnergyBar(player.energy, player.maxEnergy) + const statusIcons = (player.statusEffects ?? []) + .map((e) => { + switch (e.type) { + case 'poison': + return `🧪x${e.stacks ?? 1}` + case 'inspired': + return '✨' + case 'blessed': + return '🥋' + case 'shielded': + return '🛡️' + case 'decoy': + return '🪞' + case 'resting': + return '💤' + case 'exposed': + return '⚠️' + case 'damageBoost': + return '💪' + case 'dodgeAll': + return '🌫️' + default: + return '' + } + }) + .filter(Boolean) + .join(' ') + + const alive = player.alive ? '' : ' 💀' + const cooldown = player.specialCooldown > 0 ? ` | Special: ${player.specialCooldown}r` : ' | Special: READY' + + return `${player.name}${classTag}${alive}\n HP: ${hpBar} ${player.hp}/${player.maxHp} | Energy: ${energyBar} ${player.energy}/${player.maxEnergy}${cooldown}${statusIcons ? ` | ${statusIcons}` : ''}` +} diff --git a/bots/quack-norris/src/lib/npcs.ts b/bots/quack-norris/src/lib/npcs.ts new file mode 100644 index 00000000000..232cc405fc6 --- /dev/null +++ b/bots/quack-norris/src/lib/npcs.ts @@ -0,0 +1,278 @@ +import type { ItemType } from './items' +import type { LocationId } from './locations' + +export type NpcId = 'duchess' | 'chad' | 'gerald' | 'bigmouth' | 'harold' | 'frostbeak' | 'trenchbill' | 'attenbird' + +export type NpcDialogue = { + greeting: string + noQuest: string + questAvailable: string + questInProgress: string + questComplete: string + postQuestDialogue?: string +} + +export type ShopItem = { + itemType: ItemType + cost: number + quantity?: number + label?: string +} + +export type NpcDefinition = { + id: NpcId + name: string + emoji: string + location: LocationId + description: string + dialogue: NpcDialogue + shopInventory?: ShopItem[] + shopRequiresQuestId?: string +} + +export const NPCS: Record = { + duchess: { + id: 'duchess', + name: 'Duchess Featherington', + emoji: '👑', + location: 'coliseum', + description: 'The imperious socialite of the Coliseum. She judges everyone and everything.', + dialogue: { + greeting: + '*Duchess Featherington adjusts her monocle and fixes you with a withering stare.* "Ah, you again. I suppose you want something. Everyone always wants something."', + noQuest: '"I have nothing for you at the moment, darling. Go punch something. Preferably not me."', + questAvailable: + '*The Duchess lowers her voice conspiratorially.* "I might have a... proposition for you. Something that requires a certain... lack of dignity. Interested?"', + questInProgress: + '"Still working on that little errand? Do try to keep up, darling. My patience has an expiration date."', + questComplete: + '*The Duchess claps once, sharply.* "Splendid! You managed not to disappoint. A rare occurrence in this cesspool of mediocrity."', + postQuestDialogue: + 'Ah, my champion returns. The Coliseum remembers what you did for us. *adjusts tiara* Need anything?', + }, + shopInventory: [ + { itemType: 'hpPotion', cost: 40, quantity: 2, label: 'Royal Healing Draught (heals double)' }, + { itemType: 'energyDrink', cost: 40, quantity: 2, label: 'Coliseum Energy Elixir (restores double)' }, + { itemType: 'shieldToken', cost: 60, quantity: 1, label: "Duchess's Royal Shield" }, + { itemType: 'damageBoost', cost: 60, quantity: 1, label: "Champion's War Banner" }, + ], + shopRequiresQuestId: 'duchess_favor', + }, + chad: { + id: 'chad', + name: 'Chad Gullsworth', + emoji: '🦅', + location: 'puddle', + description: 'A loud, obnoxious seagull who treats the Puddle of Doom like his personal kingdom.', + dialogue: { + greeting: + '*Chad Gullsworth swoops down and lands on the Honda Civic.* "WELL WELL WELL! Look who decided to grace MY puddle with their presence!"', + noQuest: '"I got NOTHING for you right now. But stick around, I\'m SURE something will come up. IT ALWAYS DOES."', + questAvailable: + '*Chad puffs up his chest.* "HEY! Yeah, YOU! I got a JOB for you. And by job I mean a PROBLEM that I need someone ELSE to solve!"', + questInProgress: + '"You still haven\'t done that thing? COME ON! I could have done it myself by now! ...I just choose not to."', + questComplete: + '*Chad looks genuinely surprised.* "Wait, you actually DID it? Huh. Okay. I guess you\'re not COMPLETELY useless. Here."', + postQuestDialogue: + "BRO! You actually believed in me when nobody else did. That's... *sniff* ...that's tight, bro. Chad remembers.", + }, + }, + gerald: { + id: 'gerald', + name: 'Gerald', + emoji: '🪿', + location: 'highway', + description: 'A wise flying goose who communicates exclusively in honks. He understands everything.', + dialogue: { + greeting: '*Gerald banks left and glances at you with one ancient, knowing eye.* "HONK."', + noQuest: '"HONK." *Gerald shakes his head slowly. There is nothing for you here. Not yet.*', + questAvailable: + '*Gerald circles three times — the universal goose signal for "I have something important." He honks twice, urgently.*', + questInProgress: '"HONK." *Gerald gives you a look that somehow conveys both patience and mild disappointment.*', + questComplete: '"HONK HONK!" *Gerald does a barrel roll of joy. A feather drifts down. It feels like approval.*', + postQuestDialogue: "*HONK* (It's a warm honk. Gerald remembers.)", + }, + }, + bigmouth: { + id: 'bigmouth', + name: 'Big Mouth McGee', + emoji: '🐦', + location: 'quackatoa', + description: 'An enormous pelican who lives in the volcanic lake. Everything is a riddle or a deal with him.', + dialogue: { + greeting: + '*Big Mouth McGee surfaces from the lava, steam rising from his enormous beak.* "WELL WELL WELL! Another visitor to my VOLCANIC DOMAIN!"', + noQuest: + "\"No jobs right now. But I've got RIDDLES if you want 'em. Actually, I've got riddles even if you DON'T want 'em.\"", + questAvailable: + '*McGee\'s eyes gleam.* "I\'ve got a PROPOSITION for you. And unlike my riddles, this one has a RIGHT answer. Interested?"', + questInProgress: + '"You\'re STILL working on that? I could solve it in my SLEEP! ...I sleep in lava, so that\'s saying something."', + questComplete: + '*McGee opens his enormous beak in what might be a smile.* "NOT BAD! Not bad at all! Here\'s your reward — straight from the pouch!"', + postQuestDialogue: "THE LEGEND RETURNS! The one who actually survived my riddles! ...well, 'riddles.' ANYWAY!", + }, + }, + harold: { + id: 'harold', + name: 'Harold', + emoji: '👴', + location: 'parkBench', + description: 'An ancient old man on a park bench. He says nothing. He sees everything.', + dialogue: { + greeting: '*Harold sits on his bench. He does not acknowledge your arrival. But he knows.*', + noQuest: '*Harold stares straight ahead. A pigeon lands on his shoulder. Neither moves.*', + questAvailable: + '*Harold slowly turns his head toward you. This has never happened before. He reaches into his coat pocket and produces a crumpled note.*', + questInProgress: '*Harold holds up one finger. Then puts it down. You understand: not yet.*', + questComplete: + '*Harold nods once. Just once. But in that nod is the weight of a thousand unspoken words and what might — if you squint — be pride.*', + postQuestDialogue: + '*Harold looks up from his chess board. For the briefest moment, you see something in his eyes. Recognition. Respect. He gestures to the empty seat across from him.*', + }, + }, + frostbeak: { + id: 'frostbeak', + name: 'Frostbeak', + emoji: '🧊', + location: 'frozenPond', + description: 'A duck frozen in ice for centuries. Still conscious. Still sarcastic.', + dialogue: { + greeting: + '*Frostbeak\'s icy eyes follow you.* "Oh! A visitor! I\'d wave but... you know. Frozen. What century is this?"', + noQuest: '"I have nothing for you right now. But stick around — I\'m not going anywhere. Ha. Ha. Frozen humor."', + questAvailable: + '*Frostbeak\'s eyes widen.* "Actually... I just remembered something. Something from before the ice. Something important. Can you help me?"', + questInProgress: + '"How\'s that thing going? Take your time. I\'ve got literally nothing but time. ...Please hurry."', + questComplete: + '*A crack runs through the ice around Frostbeak. A single tear freezes on their cheek.* "You did it. Thank you. I... remember now."', + postQuestDialogue: + "You... thawed more than just ice that day. *quiet pause* And the Egg — whatever you chose, I felt it resonate through the ice. I won't forget. What brings you back to the cold?", + }, + }, + trenchbill: { + id: 'trenchbill', + name: 'Trenchbill', + emoji: '🧥', + location: 'frozenPond', + description: + 'A shady vendor in a battered trenchcoat. Sells questionable goods at reasonable prices. No eye contact.', + dialogue: { + greeting: '*Trenchbill opens his coat slightly.* "Psst. Hey. You buyin\'? I got goods."', + noQuest: '"No jobs right now. But I got MERCHANDISE. Always got merchandise."', + questAvailable: + '*Trenchbill looks both ways, then up, then down.* "Hey. I got a job. Pays well. Don\'t ask questions. ...You\'re already asking questions with your eyes. Stop that."', + questInProgress: '"You still on that thing? Tick tock, friend. Time is breadcrumbs."', + questComplete: + '*Trenchbill nods approvingly.* "Nice work. Clean. Professional. Here\'s your cut." *He slides something across without making eye contact.*', + postQuestDialogue: + 'Well, well... my favorite business partner. *adjusts trenchcoat* The underground never forgets a friend.', + }, + shopInventory: [ + { itemType: 'hpPotion', cost: 20 }, + { itemType: 'energyDrink', cost: 20 }, + { itemType: 'shieldToken', cost: 35 }, + { itemType: 'damageBoost', cost: 35 }, + ], + }, + attenbird: { + id: 'attenbird', + name: 'Sir David Attenbird', + emoji: '🎬', + location: 'coliseum', + description: + 'A distinguished duck in a tweed jacket and tiny monocle, flanked by an invisible camera crew. Documents everything.', + dialogue: { + greeting: + '*Sir David Attenbird adjusts his tiny monocle.* "Ah, the remarkable Quacktament participant. Observe how they approach — a mixture of confidence and mild confusion. Truly fascinating."', + noQuest: + '"I\'m merely observing today. The natural world requires patience. And breadcrumbs. Mostly breadcrumbs."', + questAvailable: + '*Attenbird clears his throat.* "I\'m producing a documentary on the Quackverse. I could use... a protagonist. Would you be interested in being filmed?"', + questInProgress: + '"The documentary is coming along splendidly. Your contribution has been... adequate. Keep going."', + questComplete: + '*Attenbird removes his monocle and wipes it.* "Magnificent footage. This will win awards. You, my friend, are a natural."', + postQuestDialogue: + '*Sir Attenbird adjusts his monocle* Ah, my documentary star! The footage from our adventure has been... quite extraordinary. Riveting television, if I may say so.', + }, + }, +} + +// --- NPC Helpers --- + +export const getNpcsAtLocation = (locationId: LocationId): NpcDefinition[] => { + return Object.values(NPCS).filter((npc) => npc.location === locationId) +} + +export const getNpcById = (id: string): NpcDefinition | undefined => { + return NPCS[id as NpcId] +} + +export const resolveNpcAlias = (input: string): NpcId | undefined => { + const lower = input.toLowerCase().trim() + const aliases: Record = { + duchess: 'duchess', + featherington: 'duchess', + chad: 'chad', + gullsworth: 'chad', + gerald: 'gerald', + goose: 'gerald', + bigmouth: 'bigmouth', + mcgee: 'bigmouth', + pelican: 'bigmouth', + harold: 'harold', + oldman: 'harold', + frostbeak: 'frostbeak', + frost: 'frostbeak', + trenchbill: 'trenchbill', + trench: 'trenchbill', + vendor: 'trenchbill', + shop: 'trenchbill', + attenbird: 'attenbird', + david: 'attenbird', + documentary: 'attenbird', + } + return aliases[lower] ?? (Object.keys(NPCS).includes(lower) ? (lower as NpcId) : undefined) +} + +export const formatNpcList = (npcs: NpcDefinition[]): string => { + return npcs.map((npc) => `${npc.emoji} **${npc.name}** — ${npc.description}`).join('\n') +} + +export const formatShop = (npc: NpcDefinition, playerBreadcrumbs: number): string | undefined => { + if (!npc.shopInventory || npc.shopInventory.length === 0) { + return undefined + } + + // Inline item definitions to avoid circular imports + const itemNames: Record = { + hpPotion: { name: 'Bread Poultice', emoji: '🍞', effect: '+20 HP' }, + energyDrink: { name: 'Pond Water Espresso', emoji: '☕', effect: '+15 Energy' }, + shieldToken: { name: "Chuck's Feather Token", emoji: '🪶', effect: 'Absorb next 25 dmg' }, + damageBoost: { name: 'Bread of Fury', emoji: '🔥', effect: '+10 dmg next attack' }, + mirrorShard: { name: 'Mirror Shard', emoji: '🪞', effect: 'Reflect 50% dmg once' }, + quackGrenade: { name: 'Quack Grenade', emoji: '💣', effect: '15 AoE dmg to all foes' }, + breadcrumbMagnet: { name: 'Breadcrumb Magnet', emoji: '🧲', effect: '2x breadcrumbs next encounter' }, + fogBomb: { name: 'Fog Bomb', emoji: '🌫️', effect: 'Dodge all attacks 1 round' }, + } + + const lines = npc.shopInventory.map((item, i) => { + const def = itemNames[item.itemType]! + const affordable = playerBreadcrumbs >= item.cost ? '' : " *(can't afford)*" + const displayName = item.label ?? def.name + const qty = item.quantity && item.quantity > 1 ? ` x${item.quantity}` : '' + return `**${i + 1}.** ${def.emoji} ${displayName}${qty} — ${item.cost} 🍞${affordable}\n *${def.effect}*` + }) + + return [ + `**${npc.emoji} ${npc.name}'s Shop**`, + '', + ...lines, + '', + `**Your breadcrumbs:** ${playerBreadcrumbs} 🍞`, + '*Type a number or `!buy ` to purchase.*', + ].join('\n') +} diff --git a/bots/quack-norris/src/lib/profile.ts b/bots/quack-norris/src/lib/profile.ts new file mode 100644 index 00000000000..8b163aaf143 --- /dev/null +++ b/bots/quack-norris/src/lib/profile.ts @@ -0,0 +1,66 @@ +import { z } from '@botpress/runtime' + +// --- Quest State Schemas --- + +const QuestProgressSchema = z.object({ + questId: z.string(), + currentStepId: z.string(), + objectiveProgress: z.record(z.string(), z.number()).default({}), + startedAt: z.string(), + choicesMade: z.array(z.string()).default([]), +}) + +const CompletedQuestSchema = z.object({ + questId: z.string(), + completedAt: z.string(), + choicesMade: z.array(z.string()).default([]), +}) + +const QuestStateSchema = z + .object({ + activeQuests: z.array(QuestProgressSchema).default([]), + completedQuests: z.array(CompletedQuestSchema).default([]), + dailyResetAt: z.string().optional(), + lastBountyCompletedAt: z.string().optional(), + generatedQuestsJson: z.string().default('[]'), + }) + .default({ activeQuests: [], completedQuests: [] }) + +export type QuestState = z.infer + +export const parseQuestState = (raw: unknown): QuestState => { + return QuestStateSchema.parse(raw ?? {}) +} + +// --- Adventure State Schemas --- + +const AdventureStateSchema = z + .object({ + activeEncounterId: z.string().optional(), + encounterStep: z.number().default(0), + encountersCompleted: z.array(z.string()).default([]), + lastExploreAt: z.string().optional(), + currentNpc: z.string().optional(), + awaitingChoice: z.enum(['encounter', 'travel', 'quest_choice', 'quest_accept', 'shop', 'none']).default('none'), + pendingQuestId: z.string().optional(), + pendingNpcId: z.string().optional(), + visitedGatedLocations: z.array(z.string()).default([]), + }) + .default({ encounterStep: 0, encountersCompleted: [], awaitingChoice: 'none', visitedGatedLocations: [] }) + +export type AdventureState = z.infer + +export const parseAdventureState = (raw: unknown): AdventureState => { + return AdventureStateSchema.parse(raw ?? {}) +} + +// --- Message Payload Schema --- + +const MessagePayloadSchema = z.object({ + text: z.string().optional(), +}) + +export const parseMessagePayload = (raw: unknown): { text?: string } => { + const result = MessagePayloadSchema.safeParse(raw) + return result.success ? result.data : {} +} diff --git a/bots/quack-norris/src/lib/progression.ts b/bots/quack-norris/src/lib/progression.ts new file mode 100644 index 00000000000..e1da2f3153d --- /dev/null +++ b/bots/quack-norris/src/lib/progression.ts @@ -0,0 +1,293 @@ +import type { ItemType } from './items' + +// --- XP & Level System --- + +export const XP_PER_LEVEL = [0, 100, 250, 450, 700, 1000, 1400, 1900, 2500, 3200] +export const MAX_LEVEL = 10 + +export const getLevelForXp = (xp: number): number => { + for (let i = XP_PER_LEVEL.length - 1; i >= 0; i--) { + if (xp >= XP_PER_LEVEL[i]!) { + return i + 1 + } + } + return 1 +} + +export const getXpToNextLevel = (xp: number): { current: number; needed: number; progress: number } => { + const level = getLevelForXp(xp) + if (level >= MAX_LEVEL) { + return { current: xp, needed: 0, progress: 1 } + } + const currentThreshold = XP_PER_LEVEL[level - 1]! + const nextThreshold = XP_PER_LEVEL[level]! + return { + current: xp - currentThreshold, + needed: nextThreshold - currentThreshold, + progress: (xp - currentThreshold) / (nextThreshold - currentThreshold), + } +} + +export const renderXpBar = (xp: number): string => { + const level = getLevelForXp(xp) + if (level >= MAX_LEVEL) { + return '██████████ MAX' + } + const { current, needed } = getXpToNextLevel(xp) + const filled = Math.round((current / needed) * 10) + return '█'.repeat(filled) + '░'.repeat(10 - filled) + ` ${current}/${needed}` +} + +// --- XP Award Helper --- + +export type XpAwardResult = { + newXp: number + newLevel: number + leveledUp: boolean + oldLevel: number +} + +export const awardXp = (currentXp: number, amount: number): XpAwardResult => { + const oldLevel = getLevelForXp(currentXp) + const newXp = currentXp + amount + const newLevel = getLevelForXp(newXp) + return { newXp, newLevel, leveledUp: newLevel > oldLevel, oldLevel } +} + +const LEVEL_UNLOCKS: Record = { + 2: '*The Arena Scribe makes a note.* "This one shows promise." New quests beckon — check `!quests`!', + 3: '*A warm wind blows across the pond.* You feel the world opening up. 🏷️ Title earned: **Pond Wanderer**', + 4: '*A shadowy figure in a trenchcoat nods from the alley.* Trenchbill has work for you — dark, profitable work.', + 5: '*Whispers travel through the Quackverse.* New side quests with rare rewards have surfaced...', + 6: '*The elders take notice.* Your reputation precedes you now. Harder encounters await.', + 7: '*A tremor shakes the Great Nest.* Something ancient stirs. The Seventh Egg quest is calling...', + 8: '*The Arena crowd chants your name.* Veterans seek your counsel. Power flows through your feathers.', + 9: '*Even Chuck Norris glances your way.* The final threshold approaches. Legends are forged here.', + 10: '*The Quackverse bows.* You have reached the pinnacle. 🏷️ Title earned: **The Unquackable**', +} + +export const renderLevelUp = (oldLevel: number, newLevel: number, newXp?: number): string => { + const xp = newXp ?? XP_PER_LEVEL[newLevel - 1] ?? 0 + const xpBar = renderXpBar(xp) + const unlock = LEVEL_UNLOCKS[newLevel] + const unlockLine = unlock + ? `\n${unlock}` + : '\n*The pond ripples with new possibilities.* Check `!quests` for what awaits.' + return `\n🎉 **LEVEL UP!** You are now **Level ${newLevel}**!\n${xpBar} XP to next level${unlockLine}` +} + +// --- XP Award Amounts --- + +export const XP_AWARDS = { + encounter: 15, + questStep: 25, + sideQuestComplete: 75, + mainQuestComplete: 150, + dailyQuestComplete: 40, + tournamentWin: 100, + tournamentLoss: 25, + tournamentKill: 15, +} as const + +// --- Breadcrumb Award Amounts --- + +export const BREADCRUMB_AWARDS = { + encounter: 5, + questStep: 10, + sideQuestComplete: 30, + mainQuestComplete: 75, + dailyQuestComplete: 20, + tournamentWin: 50, +} as const + +// --- Title System --- + +export type TitleDefinition = { + id: string + name: string + condition: string +} + +export const TITLES: TitleDefinition[] = [ + { id: 'fledgling', name: 'Fledgling', condition: 'Default starting title' }, + { id: 'pond_wanderer', name: 'Pond Wanderer', condition: 'Reach level 3' }, + { id: 'breadcrumb_hunter', name: 'Breadcrumb Hunter', condition: 'Complete 10 encounters' }, + { id: 'arena_initiate', name: 'Arena Initiate', condition: 'Win 1 tournament' }, + { id: 'quack_champion', name: 'Quack Champion', condition: 'Win 5 tournaments' }, + { id: 'lorekeeper', name: 'Lorekeeper', condition: 'Complete all main quests' }, + { id: 'the_unquackable', name: 'The Unquackable', condition: 'Reach level 10' }, + { id: 'duck_of_all_trades', name: 'Duck of All Trades', condition: 'Complete 10 quests total' }, + { id: 'breadcrumb_baron', name: 'Breadcrumb Baron', condition: 'Accumulate 500 breadcrumbs' }, + { + id: 'coliseum_regular', + name: 'Coliseum Regular', + condition: "Complete The Duchess's Favor (attend in finest feathers)", + }, + { id: 'party_crasher', name: 'Party Crasher', condition: "Complete The Duchess's Favor (crash the party)" }, + { id: 'business_partner', name: 'Business Partner', condition: "Complete Trenchbill's Underworld" }, + { id: 'chads_friend', name: "Chad's Friend", condition: "Complete Chad's Redemption" }, + { id: 'documentary_star', name: 'Documentary Star', condition: 'Complete The Attenbird Documentary' }, + { id: 'harolds_apprentice', name: "Harold's Apprentice", condition: "Complete Harold's Chess Tournament" }, + { id: 'the_awakened', name: 'The Awakened', condition: 'Hatch the Seventh Egg' }, + { id: 'the_peacekeeper', name: 'The Peacekeeper', condition: 'Seal the Seventh Egg' }, +] + +export const checkTitleUnlocks = (profile: { + level: number + totalWins: number + breadcrumbs: number + titlesUnlocked: string[] + adventureState: { encountersCompleted: string[] } + questState: { completedQuests: { questId: string }[] } +}): string[] => { + const newTitles: string[] = [] + const has = (id: string): boolean => profile.titlesUnlocked.includes(id) + const unlock = (id: string): void => { + if (!has(id)) { + newTitles.push(id) + } + } + + if (profile.level >= 3) { + unlock('pond_wanderer') + } + if (profile.level >= 10) { + unlock('the_unquackable') + } + if (profile.adventureState.encountersCompleted.length >= 10) { + unlock('breadcrumb_hunter') + } + if (profile.totalWins >= 1) { + unlock('arena_initiate') + } + if (profile.totalWins >= 5) { + unlock('quack_champion') + } + if (profile.breadcrumbs >= 500) { + unlock('breadcrumb_baron') + } + if (profile.questState.completedQuests.length >= 10) { + unlock('duck_of_all_trades') + } + + const mainQuestIds = ['duchess_favor', 'frozen_prophecy', 'trenchbill_underworld', 'seventh_egg'] + const completedIds = profile.questState.completedQuests.map((q) => q.questId) + if (mainQuestIds.every((id) => completedIds.includes(id))) { + unlock('lorekeeper') + } + + return newTitles +} + +export const getTitleName = (titleId: string): string => { + return TITLES.find((t) => t.id === titleId)?.name ?? titleId +} + +// --- Milestone Celebrations --- + +type MilestoneProfile = { + breadcrumbs: number + totalWins: number + adventureState: { encountersCompleted: string[] } + questState: { completedQuests: { questId: string }[] } +} + +const MILESTONES = [ + { + check: (p: MilestoneProfile) => p.breadcrumbs >= 100, + msg: '🍞 **100 Breadcrumbs!** *The first crumbs of an empire.* The Duchess glances at your pouch with newfound respect — a fortune by fledgling standards, a mere appetiser for what lies ahead.', + }, + { + check: (p: MilestoneProfile) => p.breadcrumbs >= 250, + msg: '🍞 **250 Breadcrumbs!** *The merchants of Puddle Plaza whisper your name.* With this fortune you could buy a small pond outright — or gamble it all at the Vault. The choice, as always, is yours.', + }, + { + check: (p: MilestoneProfile) => p.breadcrumbs >= 500, + msg: '🍞 **500 Breadcrumbs!** *A golden warmth radiates from your pouch.* The Breadcrumb Baron themselves would tip their hat. Trenchbill mutters something about "investment opportunities." Wealth invites attention — not all of it friendly.', + }, + { + check: (p: MilestoneProfile) => p.adventureState.encountersCompleted.length >= 5, + msg: '🗺️ **5 Encounters!** *The fog of the unknown begins to thin.* Paths you once feared now feel familiar. The Quackverse is vast, but you are learning its rhythms — the rustle before an ambush, the glint of hidden treasure.', + }, + { + check: (p: MilestoneProfile) => p.adventureState.encountersCompleted.length >= 10, + msg: '🗺️ **10 Encounters!** *Sir David Attenbird adjusts his monocle and begins writing.* "A specimen of remarkable persistence," he murmurs into his field recorder. The world bends differently around seasoned explorers.', + }, + { + check: (p: MilestoneProfile) => p.adventureState.encountersCompleted.length >= 25, + msg: "🗺️ **25 Encounters!** *The Quackverse has no more shadows you haven't touched.* Every alley, every frozen ridge, every crumbling ruin — you have walked them all. The land itself seems to nod in quiet recognition of one who has truly seen it.", + }, + { + check: (p: MilestoneProfile) => p.totalWins >= 1, + msg: '⚔️ **First Tournament Win!** *The Arena falls silent, then erupts.* Chuck Norris shifts in his golden nest and delivers a single, slow nod. The crowd will forget many things — but never a first victory. The breadcrumbs remember.', + }, + { + check: (p: MilestoneProfile) => p.totalWins >= 5, + msg: '⚔️ **5 Tournament Wins!** *The Arena Scribe carves your name deeper into the stone.* Five victories — not luck, not chance, but craft. The crowd chants in rhythm. Opponents study your patterns before they dare step into the ring.', + }, + { + check: (p: MilestoneProfile) => p.questState.completedQuests.length >= 3, + msg: '📜 **3 Quests Complete!** *Word travels fast through the Quackverse.* The Duchess mentions you at dinner. Gerald saves you a seat. Even Trenchbill offers a grudging discount. The NPCs of this world are beginning to trust you with their deepest secrets.', + }, + { + check: (p: MilestoneProfile) => p.questState.completedQuests.length >= 10, + msg: "📜 **10 Quests Complete!** *The Great Mallard's constellation shifts overhead.* You have woven yourself into the very fabric of the Quackverse. Every NPC knows your name, every tavern tells your stories, every questline bears your mark. A true Duck of All Trades.", + }, +] as const + +/** + * Check milestones and return celebration messages for newly reached ones. + * Uses previousMilestoneIndex to avoid re-triggering and enforces ordered progression. + */ +export const checkMilestones = ( + profile: MilestoneProfile, + previousMilestoneIndex: number +): { messages: string[]; newIndex: number } => { + const messages: string[] = [] + let newIndex = previousMilestoneIndex + + // Advance only through contiguous newly satisfied milestones. + // This avoids permanently skipping an earlier milestone when a later one is reached first. + while (newIndex + 1 < MILESTONES.length && MILESTONES[newIndex + 1]!.check(profile)) { + newIndex += 1 + messages.push(MILESTONES[newIndex]!.msg) + } + + return { messages, newIndex } +} + +// --- HUD Rendering --- + +export type HudProfile = { + displayName: string + title: string + level: number + xp: number + breadcrumbs: number + currentLocation: string + inventory: { type: ItemType; quantity: number }[] + questState: { activeQuests: { questId: string; currentStepId: string }[] } +} + +export const renderHud = ( + profile: HudProfile, + locationEmoji: string, + locationName: string, + activeQuestName?: string, + questStepProgress?: string +): string => { + const titleDisplay = getTitleName(profile.title) + const xpBar = renderXpBar(profile.xp) + const invSlots = profile.inventory.reduce((sum, i) => sum + (i.quantity > 0 ? 1 : 0), 0) + + const lines = [ + `─── 🐤 ${profile.displayName} [${titleDisplay}] ───`, + `Lvl ${profile.level} ${xpBar} | 🍞 ${profile.breadcrumbs} | 📍 ${locationEmoji} ${locationName} | 📦 ${invSlots}/6`, + ] + + if (activeQuestName && questStepProgress) { + lines.push(`Active: ${activeQuestName} (${questStepProgress})`) + } + + return lines.join('\n') +} diff --git a/bots/quack-norris/src/lib/quest-engine.ts b/bots/quack-norris/src/lib/quest-engine.ts new file mode 100644 index 00000000000..ca4a0da95af --- /dev/null +++ b/bots/quack-norris/src/lib/quest-engine.ts @@ -0,0 +1,168 @@ +import type { ProfileRow, InteractionState } from './command-context' +import { addItemToInventory, ITEMS } from './items' +import { LOCATIONS, type LocationId } from './locations' +import { parseQuestState } from './profile' +import { awardXp, renderLevelUp, checkTitleUnlocks, getTitleName } from './progression' +import { + getQuestById, + getCurrentStep, + checkObjectiveProgress, + isStepComplete, + advanceToNextStep, + type QuestDefinition, + type QuestProgress, + type QuestObjectiveType, +} from './quests' + +export type QuestDefLookup = (id: string) => QuestDefinition | undefined + +// --- Quest completion (pure profile mutation — no DB writes) --- + +export const completeQuestForProfile = ( + profile: ProfileRow, + quest: QuestProgress, + def: ReturnType & object +): string[] => { + const msgs: string[] = [] + const qs = parseQuestState(profile.questState) + + qs.activeQuests = qs.activeQuests.filter((q) => q.questId !== quest.questId) + qs.completedQuests.push({ + questId: quest.questId, + completedAt: new Date().toISOString(), + choicesMade: quest.choicesMade, + }) + if (quest.questId.startsWith('bounty_')) { + ;(qs as Record).lastBountyCompletedAt = new Date().toISOString() + } + profile.questState = qs as typeof profile.questState + + let xpGain = 0 + let bcGain = 0 + for (const reward of def.rewards) { + if (reward.type === 'xp') { + xpGain += reward.value as number + } else if (reward.type === 'breadcrumbs') { + bcGain += reward.value as number + } else if (reward.type === 'title') { + const titleId = reward.value as string + if (!profile.titlesUnlocked.includes(titleId)) { + profile.titlesUnlocked = [...profile.titlesUnlocked, titleId] + msgs.push(`🏆 Title unlocked: **${getTitleName(titleId)}**`) + } + } else if (reward.type === 'locationUnlock') { + const locId = reward.value as string + if (!profile.unlockedLocations.includes(locId)) { + profile.unlockedLocations = [...profile.unlockedLocations, locId] + const loc = LOCATIONS[locId as LocationId] + msgs.push(`🔓 Location unlocked: **${loc?.emoji ?? ''} ${loc?.name ?? locId}**`) + } + } else if (reward.type === 'item') { + const added = addItemToInventory(profile.inventory, reward.value as string) + if (added) { + const itemDef = ITEMS[reward.value as keyof typeof ITEMS] + msgs.push(`📦 Item received: ${itemDef?.emoji ?? '✨'} ${itemDef?.name ?? reward.value}`) + } + } + } + + const questXpResult = awardXp(profile.xp ?? 0, xpGain) + profile.xp = questXpResult.newXp + profile.breadcrumbs = (profile.breadcrumbs ?? 0) + bcGain + profile.level = questXpResult.newLevel + + if (xpGain > 0 || bcGain > 0) { + const parts = [] + if (xpGain > 0) { + parts.push(`+${xpGain} XP`) + } + if (bcGain > 0) { + parts.push(`+${bcGain} 🍞`) + } + msgs.push(`🎁 Rewards: ${parts.join(', ')}`) + } + if (questXpResult.leveledUp) { + msgs.push(renderLevelUp(questXpResult.oldLevel, questXpResult.newLevel, questXpResult.newXp)) + } + + const newTitles = checkTitleUnlocks(profile as Parameters[0]) + for (const t of newTitles) { + if (!profile.titlesUnlocked.includes(t)) { + profile.titlesUnlocked = [...profile.titlesUnlocked, t] + msgs.push(`🏆 Title unlocked: **${getTitleName(t)}**`) + } + } + + msgs.unshift(`\n🎉 **Quest Complete: ${def.name}!**`) + return msgs +} + +// --- Quest objective advancement (pure profile mutation — no DB writes) --- + +export type AdvanceResult = { + messages: string[] + changed: boolean + interactionUpdate?: Partial +} + +export const advanceQuestObjectivesForProfile = ( + profile: ProfileRow, + eventType: QuestObjectiveType, + eventTarget?: string, + questDefLookup?: QuestDefLookup +): AdvanceResult => { + const messages: string[] = [] + let changed = false + let interactionUpdate: Partial | undefined + const lookup = questDefLookup ?? getQuestById + + if (!profile.questState) { + return { messages, changed } + } + + const qs = parseQuestState(profile.questState) + + for (const quest of qs.activeQuests) { + const def = lookup(quest.questId) + if (!def) { + continue + } + const result = checkObjectiveProgress(quest, def, eventType, eventTarget) + if (!result.updated) { + continue + } + changed = true + if (result.objectiveCompleted) { + messages.push(`✅ Quest objective complete: *${result.objectiveCompleted}*`) + } + if (!isStepComplete(quest, def)) { + continue + } + const step = getCurrentStep(quest, def) + if (step?.choices && step.choices.length > 0) { + const choiceLines = step.choices.map((c, i) => `**${i + 1}.** ${c.label}`) + messages.push(`\n📜 **${def.name}** — ${step.dialogueOnStart ?? 'A choice awaits:'}`) + messages.push(choiceLines.join('\n')) + interactionUpdate = { + awaitingChoice: 'quest_choice', + pendingQuestId: quest.questId, + } + } else { + if (step?.dialogueOnComplete) { + messages.push(`\n📜 **${def.name}** — ${step.dialogueOnComplete}`) + } + const advance = advanceToNextStep(quest, def) + if (advance.completed) { + messages.push(...completeQuestForProfile(profile, quest, def)) + } else if (advance.nextStep?.dialogueOnStart) { + messages.push(`\n📜 Next: *${advance.nextStep.description}*`) + } + } + } + + if (changed) { + profile.questState = qs as typeof profile.questState + } + + return { messages, changed, interactionUpdate } +} diff --git a/bots/quack-norris/src/lib/quest-generator.ts b/bots/quack-norris/src/lib/quest-generator.ts new file mode 100644 index 00000000000..f970126fee4 --- /dev/null +++ b/bots/quack-norris/src/lib/quest-generator.ts @@ -0,0 +1,174 @@ +import { adk, z } from '@botpress/runtime' + +import { LOCATIONS, type LocationId } from './locations' +import { NPCS, type NpcId } from './npcs' +import { parseQuestState } from './profile' +import { getQuestById, type QuestDefinition, type QuestReward } from './quests' + +// --- Zod schema for LLM output (constrains to valid game entities) --- + +const GeneratedObjectiveSchema = z.object({ + id: z.string().describe('Unique objective ID, e.g. "bounty_obj_1"'), + description: z.string().describe('Short player-facing description with duck puns'), + type: z + .enum(['talkToNpc', 'visitLocation', 'completeEncounter', 'collectItem']) + .describe('Objective type — only these 4 are allowed'), + target: z + .enum([ + 'duchess', + 'chad', + 'gerald', + 'bigmouth', + 'harold', + 'frostbeak', + 'trenchbill', + 'attenbird', + 'coliseum', + 'puddle', + 'highway', + 'quackatoa', + 'parkBench', + 'frozenPond', + 'hpPotion', + 'energyDrink', + 'shieldToken', + 'damageBoost', + 'mirrorShard', + 'quackGrenade', + 'breadcrumbMagnet', + 'fogBomb', + ]) + .optional() + .describe('Target NPC, location, or item ID. Omit for "any" target.'), + count: z.number().min(1).max(5).describe('How many times to complete (1-5)'), +}) + +const GeneratedStepSchema = z.object({ + id: z.string().describe('Unique step ID, e.g. "bounty_step_1"'), + description: z.string().describe('Short step description'), + objectives: z.array(GeneratedObjectiveSchema).min(1).max(2), + dialogueOnStart: z.string().optional().describe('NPC dialogue when step begins'), + dialogueOnComplete: z.string().optional().describe('NPC dialogue when step ends'), +}) + +const GeneratedBountySchema = z.object({ + name: z.string().max(50).describe('Quest name with duck flavor'), + emoji: z.string().max(4).describe('A single emoji for the quest'), + description: z.string().max(200).describe('1-2 sentence quest description'), + steps: z.array(GeneratedStepSchema).min(1).max(3), + difficultyTier: z.number().min(1).max(3).describe('1=easy, 2=medium, 3=hard'), + flavorText: z.string().max(300).describe('Opening narrative when quest is offered'), +}) + +type GeneratedBounty = z.infer + +// --- Reward scaling --- + +const BOUNTY_REWARD_TABLE: Record = { + 1: { xpBase: 25, breadcrumbBase: 15 }, + 2: { xpBase: 40, breadcrumbBase: 25 }, + 3: { xpBase: 60, breadcrumbBase: 40 }, +} + +export const computeBountyRewards = (playerLevel: number, difficultyTier: number): QuestReward[] => { + const base = BOUNTY_REWARD_TABLE[difficultyTier] ?? BOUNTY_REWARD_TABLE[1]! + const levelMultiplier = 1 + (playerLevel - 1) * 0.15 + return [ + { type: 'xp', value: Math.round(base.xpBase * levelMultiplier) }, + { type: 'breadcrumbs', value: Math.round(base.breadcrumbBase * levelMultiplier) }, + ] +} + +// --- Quest generation via zai.extract --- + +export const generateBountyQuest = async ( + giverNpcId: NpcId, + playerLevel: number, + playerLocation: LocationId, + completedBountyNames: string[] +): Promise<{ generated: GeneratedBounty; definition: QuestDefinition }> => { + const npc = NPCS[giverNpcId] + const locationList = Object.values(LOCATIONS) + .filter((l) => !l.requiresQuestId) + .map((l) => `${l.id} (${l.name})`) + .join(', ') + + const npcList = Object.values(NPCS) + .map((n) => `${n.id} (${n.name})`) + .join(', ') + + const prompt = `You are a quest designer for "Quack Norris," a duck-themed RPG set in The Pond Eternal. + +WORLD CONTEXT: +- Silly, pun-filled duck RPG. Chuck Norris is an ancient duck deity. +- Absurdist humor meets classic RPG. +- NPCs: ${npcList} +- Locations: ${locationList} + +QUEST GIVER: ${npc.name} at ${LOCATIONS[playerLocation]?.name ?? playerLocation} +PLAYER LEVEL: ${playerLevel} +PAST BOUNTIES: ${completedBountyNames.length > 0 ? completedBountyNames.join(', ') : 'None'} + +RULES: +- 1-3 steps, 1-2 objectives per step. +- Objective types: talkToNpc, visitLocation, completeEncounter, collectItem ONLY. +- Targets must be real IDs from the lists above. +- Thematic to the quest giver NPC personality. +- Duck puns and silly RPG flavor. Dialogue in character for ${npc.name}. +- Do NOT repeat themes from past bounties. +- Difficulty: tier 1 for lvl 1-3, tier 2 for lvl 4-6, tier 3 for lvl 7+. +- Counts: 1-2 for talkToNpc/visitLocation, 1-3 for completeEncounter, 1 for collectItem. +- Step IDs: bounty_step_1, bounty_step_2, etc. Objective IDs: bounty_obj_1, bounty_obj_2, etc.` + + const generated = await adk.zai.extract(prompt, GeneratedBountySchema) + + const questId = `bounty_${giverNpcId}_${Date.now()}` + const rewards = computeBountyRewards(playerLevel, generated.difficultyTier) + + const definition: QuestDefinition = { + id: questId, + name: generated.name, + emoji: generated.emoji, + category: 'side', + description: generated.description, + giverNpc: giverNpcId, + giverLocation: playerLocation, + levelRequired: 1, + prerequisiteQuestIds: [], + steps: generated.steps.map((s) => ({ + ...s, + choices: undefined, + })), + rewards, + repeatable: false, + } + + return { generated, definition } +} + +// --- Quest lookup that includes generated quests --- + +export const buildQuestLookup = (generatedQuests: QuestDefinition[]): ((id: string) => QuestDefinition | undefined) => { + return (id: string): QuestDefinition | undefined => { + return getQuestById(id) ?? generatedQuests.find((g) => g.id === id) + } +} + +// --- Helper to extract generated quests from profile questState --- + +export const getGeneratedQuestsFromProfile = (questState: unknown): QuestDefinition[] => { + const qs = parseQuestState(questState) + const json = (qs as Record).generatedQuestsJson as string | undefined + if (!json || json === '[]') { + return [] + } + try { + return JSON.parse(json) as QuestDefinition[] + } catch { + return [] + } +} + +export const serializeGeneratedQuests = (quests: QuestDefinition[]): string => { + return JSON.stringify(quests) +} diff --git a/bots/quack-norris/src/lib/quests.ts b/bots/quack-norris/src/lib/quests.ts new file mode 100644 index 00000000000..b4aa027d9f9 --- /dev/null +++ b/bots/quack-norris/src/lib/quests.ts @@ -0,0 +1,1150 @@ +import type { LocationId } from './locations' +import type { NpcId } from './npcs' + +// --- Quest Types --- + +export type QuestObjectiveType = + | 'talkToNpc' + | 'visitLocation' + | 'completeEncounter' + | 'winTournament' + | 'collectItem' + | 'reachLevel' + | 'spendBreadcrumbs' + | 'defeatInTournament' + +export type QuestObjective = { + id: string + description: string + type: QuestObjectiveType + target?: string + count: number +} + +export type QuestReward = { + type: 'xp' | 'item' | 'breadcrumbs' | 'title' | 'locationUnlock' + value: string | number + quantity?: number +} + +export type QuestChoice = { + label: string + nextStepId: string + narrative: string + rewards?: QuestReward[] +} + +export type QuestStep = { + id: string + description: string + objectives: QuestObjective[] + dialogueOnStart?: string + dialogueOnComplete?: string + choices?: QuestChoice[] +} + +export type QuestCategory = 'main' | 'side' | 'daily' + +export type QuestDefinition = { + id: string + name: string + emoji: string + category: QuestCategory + description: string + giverNpc: NpcId + giverLocation: LocationId + levelRequired: number + prerequisiteQuestIds: string[] + requiresChoiceMade?: { questId: string; choiceLabel: string } + steps: QuestStep[] + rewards: QuestReward[] + repeatable: boolean + cooldownHours?: number +} + +export type QuestProgress = { + questId: string + currentStepId: string + objectiveProgress: Record + startedAt: string + choicesMade: string[] +} + +export type CompletedQuest = { + questId: string + completedAt: string + choicesMade: string[] +} + +// --- Main Quest Chains --- + +const MAIN_QUESTS: QuestDefinition[] = [ + { + id: 'duchess_favor', + name: "The Duchess's Favor", + emoji: '👑', + category: 'main', + description: + 'Duchess Featherington has a problem. Chad Gullsworth stole her prized monocle case. Recover it and earn standing in the Coliseum.', + giverNpc: 'duchess', + giverLocation: 'coliseum', + levelRequired: 2, + prerequisiteQuestIds: [], + steps: [ + { + id: 'audience', + description: 'Speak with Duchess Featherington at the Coliseum', + objectives: [ + { id: 'talk_duchess', description: 'Talk to the Duchess', type: 'talkToNpc', target: 'duchess', count: 1 }, + ], + dialogueOnStart: + '*The Duchess fixes you with a calculating stare.* "You want standing in this arena? Then prove you\'re not just another breadcrumb. That WRETCHED seagull — Chad Gullsworth — stole my monocle case. Retrieve it and we\'ll talk."', + dialogueOnComplete: + '"Well? What are you waiting for? The Puddle of Doom. That\'s where the feathered delinquent lurks."', + }, + { + id: 'confront_chad', + description: 'Travel to the Puddle of Doom and confront Chad', + objectives: [ + { + id: 'visit_puddle', + description: 'Travel to the Puddle', + type: 'visitLocation', + target: 'puddle', + count: 1, + }, + { id: 'talk_chad', description: 'Talk to Chad', type: 'talkToNpc', target: 'chad', count: 1 }, + ], + dialogueOnStart: + '*Chad spots you from atop the Honda Civic.* "THE MONOCLE CASE? I found it FAIR AND SQUARE! In her purse! While she wasn\'t looking! That\'s basically the same as finding it on the ground!"', + dialogueOnComplete: + '"Look, I\'ll TRADE you for it. Bring me something good from around here. I\'m a duck of REFINED TASTE."', + }, + { + id: 'trade_goods', + description: "Complete 2 encounters at the Puddle to find something for Chad's trade", + objectives: [ + { + id: 'puddle_encounters', + description: 'Complete 2 encounters at the Puddle', + type: 'completeEncounter', + target: 'puddle', + count: 2, + }, + ], + dialogueOnComplete: + "\"OHHH SHINY! Yeah okay fine, that'll do. Here's your stupid monocle case. Tell the Duchess I said... actually, don't tell her anything.\"", + }, + { + id: 'return_case', + description: 'Return the monocle case to the Duchess at the Coliseum', + objectives: [ + { + id: 'return_coliseum', + description: 'Return to the Coliseum', + type: 'visitLocation', + target: 'coliseum', + count: 1, + }, + { + id: 'talk_duchess_return', + description: 'Talk to the Duchess', + type: 'talkToNpc', + target: 'duchess', + count: 1, + }, + ], + dialogueOnStart: + '*The Duchess inspects the monocle case. Her beak wrinkles.* "It smells like parking lot. But... you retrieved it. You have earned a sliver of my respect."', + dialogueOnComplete: "\"I'm hosting a gala tonight. The upper gallery. You're invited. ...Don't embarrass me.\"", + }, + { + id: 'gala_choice', + description: "Attend the Duchess's Gala", + objectives: [], + dialogueOnStart: "The Duchess's Gala awaits in the upper gallery. How will you make your entrance?", + choices: [ + { + label: 'Attend in your finest feathers', + nextStepId: 'complete', + narrative: + 'You arrive looking spectacular. The Duchess introduces you to her inner circle. *"This one has potential,"* she declares. The crowd applauds. The doors to the **Breadcrumb Vault** swing open as a sign of trust.', + rewards: [ + { type: 'title', value: 'coliseum_regular' }, + { type: 'breadcrumbs', value: 50 }, + ], + }, + { + label: 'Crash the party fashionably late', + nextStepId: 'complete', + narrative: + 'You smash through the window on Gerald\'s back. Breadcrumbs scatter. The Duchess gasps — then laughs. *"The AUDACITY! I respect it."* She tosses you a Bread of Fury. The **Breadcrumb Vault** opens for those who dare.', + rewards: [ + { type: 'title', value: 'party_crasher' }, + { type: 'item', value: 'damageBoost', quantity: 1 }, + { type: 'breadcrumbs', value: 25 }, + ], + }, + ], + }, + ], + rewards: [ + { type: 'xp', value: 150 }, + { type: 'breadcrumbs', value: 75 }, + { type: 'locationUnlock', value: 'breadcrumbVault' }, + ], + repeatable: false, + }, + { + id: 'frozen_prophecy', + name: 'The Frozen Prophecy', + emoji: '🧊', + category: 'main', + description: + 'Frostbeak remembers fragments of an ancient prophecy from before the ice. A journey across the Quackverse to uncover what The Great Mallard left behind.', + giverNpc: 'frostbeak', + giverLocation: 'frozenPond', + levelRequired: 3, + prerequisiteQuestIds: ['duchess_favor'], + steps: [ + { + id: 'echoes', + description: 'Speak with Frostbeak at the Frozen Pond', + objectives: [ + { id: 'talk_frost', description: 'Talk to Frostbeak', type: 'talkToNpc', target: 'frostbeak', count: 1 }, + ], + dialogueOnStart: + '*Frostbeak\'s eyes glow blue.* "I was frozen during the First Quacktament. I SAW things. The Seventh Egg — it never hatched. Something sleeps inside. I can\'t remember where, but Gerald might know. He flies the ancient routes."', + }, + { + id: 'manuscript', + description: 'Find the Thundercloud Manuscript on the Highway', + objectives: [ + { + id: 'visit_highway', + description: 'Travel to the Highway', + type: 'visitLocation', + target: 'highway', + count: 1, + }, + { + id: 'highway_encounter', + description: 'Discover the Thundercloud Manuscript', + type: 'completeEncounter', + target: 'highway', + count: 1, + }, + ], + dialogueOnComplete: + "In the thunderclouds, you find a scroll wrapped in ancient feathers. The Migration Manuscript — written in Old Quack. You can't read it, but you know someone who can.", + }, + { + id: 'translation', + description: 'Have Big Mouth McGee translate the scroll at Lake Quackatoa', + objectives: [ + { + id: 'visit_quackatoa', + description: 'Travel to Quackatoa', + type: 'visitLocation', + target: 'quackatoa', + count: 1, + }, + { id: 'talk_bigmouth', description: 'Talk to McGee', type: 'talkToNpc', target: 'bigmouth', count: 1 }, + ], + dialogueOnStart: + '*McGee squints at the scroll.* "LET ME READ THIS... it says: \'The Seventh Egg never fully hatched. What remains sleeps in the Great Nest, beyond the Frozen Gate, guarded by silence and the one who remembers all but speaks nothing.\'"', + dialogueOnComplete: '"The one who remembers all but speaks nothing... that sounds like HAROLD. Go find him."', + }, + { + id: 'harold_key', + description: 'Visit Harold at the Park Bench', + objectives: [ + { + id: 'visit_park', + description: 'Travel to the Park Bench', + type: 'visitLocation', + target: 'parkBench', + count: 1, + }, + { id: 'talk_harold', description: 'Talk to Harold', type: 'talkToNpc', target: 'harold', count: 1 }, + ], + dialogueOnStart: + "*Harold turns his head. For the first time in recorded history, he moves. He reaches into his coat and produces a key made of petrified bread. He places it in your wing. He says nothing. He doesn't need to.*", + }, + { + id: 'arena_trial', + description: 'Win a tournament to prove your worth', + objectives: [{ id: 'win_tournament', description: 'Win a tournament', type: 'winTournament', count: 1 }], + dialogueOnComplete: 'The Arena acknowledges your strength. The path to the Great Nest is open.', + }, + { + id: 'great_nest', + description: 'Travel to the Great Nest', + objectives: [ + { + id: 'visit_nest', + description: 'Enter the Great Nest', + type: 'visitLocation', + target: 'greatNest', + count: 1, + }, + ], + dialogueOnComplete: + 'You insert the petrified bread key. The ice cracks. The Frozen Gate opens. Beyond it: a nest so vast it fills the horizon. Golden feathers drift like snow. Something hums deep within. The Great Nest has waited a long, long time.', + }, + ], + rewards: [ + { type: 'xp', value: 150 }, + { type: 'breadcrumbs', value: 100 }, + { type: 'locationUnlock', value: 'greatNest' }, + { type: 'title', value: 'lorekeeper' }, + ], + repeatable: false, + }, + { + id: 'trenchbill_underworld', + name: "Trenchbill's Underworld", + emoji: '🧥', + category: 'main', + description: + 'Trenchbill needs startup capital for a business expansion. Help him build his empire — but watch out for double crosses.', + giverNpc: 'trenchbill', + giverLocation: 'frozenPond', + levelRequired: 4, + prerequisiteQuestIds: [], + steps: [ + { + id: 'proposition', + description: 'Speak with Trenchbill at the Frozen Pond', + objectives: [ + { id: 'talk_trench', description: 'Talk to Trenchbill', type: 'talkToNpc', target: 'trenchbill', count: 1 }, + ], + dialogueOnStart: + '*Trenchbill leans in close.* "I\'m expanding operations. Going LEGIT. ...Mostly legit. I need 50 breadcrumbs for startup capital. You fund me, I cut you in. Capisce?"', + }, + { + id: 'funding', + description: 'Give Trenchbill 50 breadcrumbs', + objectives: [ + { + id: 'pay_breadcrumbs', + description: 'Invest 50 breadcrumbs', + type: 'spendBreadcrumbs', + target: '50', + count: 1, + }, + ], + dialogueOnComplete: + '"Beautiful. Now I need you to pick up some... merchandise. Three locations. Don\'t open the packages."', + }, + { + id: 'supply_run', + description: 'Visit 3 locations to pick up supplies', + objectives: [ + { id: 'visit_1', description: 'Pick up at the Puddle', type: 'visitLocation', target: 'puddle', count: 1 }, + { id: 'visit_2', description: 'Pick up at Quackatoa', type: 'visitLocation', target: 'quackatoa', count: 1 }, + { id: 'visit_3', description: 'Pick up at the Highway', type: 'visitLocation', target: 'highway', count: 1 }, + ], + dialogueOnComplete: 'All three packages secured. But on the way back, a familiar voice shouts from behind...', + }, + { + id: 'double_cross', + description: 'Deal with Chad', + objectives: [], + dialogueOnStart: + '*Chad swoops down.* "NICE PACKAGES! Those look EXPENSIVE! I propose a... redistribution of goods!"', + choices: [ + { + label: 'Fight Chad off', + nextStepId: 'delivery', + narrative: + 'You square up. Chad hesitates — he wasn\'t expecting resistance. "FINE! Keep your stupid packages!" He flies off in a huff, dropping a Bread of Fury in his retreat.', + rewards: [ + { type: 'item', value: 'damageBoost', quantity: 1 }, + { type: 'breadcrumbs', value: 25 }, + ], + }, + { + label: 'Cut Chad in for a share', + nextStepId: 'delivery', + narrative: + 'You toss Chad a package. His eyes widen. "Wait... you\'re SHARING? Nobody ever SHARES with me." He\'s so moved he hands you a feather token. *"We\'re PARTNERS now!"*', + rewards: [{ type: 'item', value: 'shieldToken', quantity: 1 }], + }, + ], + }, + { + id: 'delivery', + description: 'Deliver the packages to Trenchbill', + objectives: [ + { id: 'return_trench', description: 'Talk to Trenchbill', type: 'talkToNpc', target: 'trenchbill', count: 1 }, + ], + dialogueOnStart: + '*Trenchbill inspects the packages. He nods slowly.* "Clean work. Professional. You\'re good at this." *His coat rustles with what might be pride.* "From now on, you get the FRIENDS AND FAMILY discount."', + }, + ], + rewards: [ + { type: 'xp', value: 150 }, + { type: 'breadcrumbs', value: 50 }, + { type: 'title', value: 'business_partner' }, + ], + repeatable: false, + }, + { + id: 'seventh_egg', + name: 'The Seventh Egg', + emoji: '🥚', + category: 'main', + description: + "The endgame. The Great Mallard's final secret lies in the Great Nest. Chuck Norris knows the truth. Are you ready?", + giverNpc: 'frostbeak', + giverLocation: 'frozenPond', + levelRequired: 7, + prerequisiteQuestIds: ['frozen_prophecy'], + steps: [ + { + id: 'nest_explore', + description: 'Explore the Great Nest', + objectives: [ + { + id: 'nest_encounter', + description: 'Complete an encounter in the Great Nest', + type: 'completeEncounter', + target: 'greatNest', + count: 1, + }, + ], + dialogueOnComplete: + 'Deep in the nest, you find inscriptions on golden shells. Four names. Four locations. Four fragments.', + }, + { + id: 'gather_flock', + description: 'Gather knowledge from the four elders', + objectives: [ + { id: 'elder_duchess', description: 'Consult the Duchess', type: 'talkToNpc', target: 'duchess', count: 1 }, + { id: 'elder_frost', description: 'Consult Frostbeak', type: 'talkToNpc', target: 'frostbeak', count: 1 }, + { id: 'elder_bigmouth', description: 'Consult McGee', type: 'talkToNpc', target: 'bigmouth', count: 1 }, + { id: 'elder_harold', description: 'Consult Harold', type: 'talkToNpc', target: 'harold', count: 1 }, + ], + dialogueOnComplete: + 'Each elder shared a fragment. Together they form a map — the Seventh Egg lies at the heart of the Great Nest. But it demands offerings.', + }, + { + id: 'offerings', + description: 'Gather offerings (items will be consumed)', + objectives: [ + { + id: 'offer_shield', + description: 'Obtain a Feather Token', + type: 'collectItem', + target: 'shieldToken', + count: 1, + }, + { + id: 'offer_fury', + description: 'Obtain a Bread of Fury', + type: 'collectItem', + target: 'damageBoost', + count: 1, + }, + { + id: 'offer_potion', + description: 'Obtain a Bread Poultice', + type: 'collectItem', + target: 'hpPotion', + count: 1, + }, + ], + dialogueOnStart: + '*The golden inscriptions glow faintly.* "The Egg demands tribute — a feather of protection, bread forged in fury, and a poultice of healing. Only the prepared may approach."', + dialogueOnComplete: + '*You place the offerings before the ancient nest. They dissolve into golden light. The path forward trembles open.* "The Egg accepts. But one trial remains."', + }, + { + id: 'trial_combat', + description: 'Prove your worth in combat', + objectives: [{ id: 'win_2', description: 'Win 2 tournaments', type: 'winTournament', count: 2 }], + dialogueOnStart: + '*A booming voice echoes through the Great Nest:* "Words and gifts are not enough. The Seventh Egg demands PROOF. Win twice in the arena, and the Egg shall reveal itself."', + dialogueOnComplete: + '*The arena falls silent. Two victories. The Great Nest shudders — somewhere deep within, an ancient heartbeat begins.*', + }, + { + id: 'the_hatching', + description: 'Return to the Great Nest', + objectives: [ + { + id: 'return_nest', + description: 'Enter the Great Nest', + type: 'visitLocation', + target: 'greatNest', + count: 1, + }, + ], + dialogueOnComplete: + 'Chuck Norris descends from his golden nest. He removes his tiny sunglasses. For the first time ever, he speaks more than one word: *"You found it. The part of me I left behind. The Egg of Violence holds one more truth."*', + }, + { + id: 'egg_choice', + description: 'Decide the fate of the Seventh Egg', + objectives: [], + dialogueOnStart: + 'The Seventh Egg pulses with ancient power. Chuck Norris watches. The entire Quackverse holds its breath.', + choices: [ + { + label: 'Hatch the Egg — unleash its power', + nextStepId: 'epilogue', + narrative: + 'The egg CRACKS. Light floods the Great Nest. A wave of primal energy surges through you. You feel stronger. Faster. More... duck. Chuck Norris nods. *"The violence was inside you all along."*', + rewards: [ + { type: 'title', value: 'the_awakened' }, + { type: 'xp', value: 200 }, + ], + }, + { + label: 'Seal the Egg — protect the Quackverse', + nextStepId: 'epilogue', + narrative: + 'You place your wing on the egg. It glows, then dims. The power recedes. Chuck Norris smiles — actually smiles. *"Strength isn\'t always about power. Sometimes it\'s about knowing when not to use it."* The Quackverse sighs with relief.', + rewards: [ + { type: 'title', value: 'the_peacekeeper' }, + { type: 'breadcrumbs', value: 200 }, + ], + }, + ], + }, + { + id: 'epilogue', + description: 'Speak with Sir David Attenbird', + objectives: [ + { id: 'talk_attenbird', description: 'Talk to Attenbird', type: 'talkToNpc', target: 'attenbird', count: 1 }, + ], + dialogueOnStart: + '*Sir David Attenbird appears, visibly emotional.* "And so concludes the most remarkable journey I have ever documented. From a fledgling at the Coliseum gates to the duck who decided the fate of the Seventh Egg. Extraordinary."', + }, + ], + rewards: [ + { type: 'xp', value: 300 }, + { type: 'breadcrumbs', value: 150 }, + ], + repeatable: false, + }, +] + +// --- Side Quests --- + +const SIDE_QUESTS: QuestDefinition[] = [ + { + id: 'gerald_secret', + name: "Gerald's Secret", + emoji: '🪿', + category: 'side', + description: 'Gerald honked 7 times. That means something. Explore the Highway to find out what.', + giverNpc: 'gerald', + giverLocation: 'highway', + levelRequired: 2, + prerequisiteQuestIds: [], + steps: [ + { + id: 'honks', + description: 'Talk to Gerald on the Highway', + objectives: [ + { id: 'talk_gerald', description: 'Talk to Gerald', type: 'talkToNpc', target: 'gerald', count: 1 }, + ], + dialogueOnStart: + '*Gerald honks 7 times. Seven. He circles a specific cloud formation. Something is hidden up here.*', + }, + { + id: 'search', + description: 'Explore the Highway for what Gerald found', + objectives: [ + { + id: 'highway_enc', + description: 'Complete an encounter on the Highway', + type: 'completeEncounter', + target: 'highway', + count: 1, + }, + ], + dialogueOnComplete: + 'In the clouds where Gerald circled, you find a small nest containing an ancient espresso. Gerald honks approvingly. The secret was caffeine all along.', + }, + ], + rewards: [ + { type: 'xp', value: 75 }, + { type: 'breadcrumbs', value: 30 }, + { type: 'item', value: 'energyDrink', quantity: 1 }, + ], + repeatable: false, + }, + { + id: 'harold_chess', + name: "Harold's Chess Tournament", + emoji: '♟️', + category: 'side', + description: 'Harold has set up a chessboard. He has been waiting. Complete his challenge.', + giverNpc: 'harold', + giverLocation: 'parkBench', + levelRequired: 3, + prerequisiteQuestIds: [], + steps: [ + { + id: 'chess_start', + description: 'Accept the chess challenge', + objectives: [ + { id: 'talk_harold', description: 'Talk to Harold', type: 'talkToNpc', target: 'harold', count: 1 }, + ], + dialogueOnStart: + '*Harold has arranged breadcrumb chess pieces on the bench. He pushes a pawn forward. It is your move.*', + }, + { + id: 'chess_play', + description: "Play Harold's chess challenge at the Park Bench", + objectives: [ + { + id: 'park_enc', + description: "Play Harold's chess game", + type: 'completeEncounter', + target: 'parkBench', + count: 1, + }, + ], + }, + { + id: 'chess_prove', + description: 'Win a tournament (Harold is watching)', + objectives: [{ id: 'win_for_harold', description: 'Win a tournament', type: 'winTournament', count: 1 }], + dialogueOnComplete: + '*Harold nods once. He slides a crumpled note across the bench. It reads: "You\'ll do." Coming from Harold, that\'s a standing ovation.*', + }, + ], + rewards: [ + { type: 'xp', value: 75 }, + { type: 'breadcrumbs', value: 40 }, + { type: 'title', value: 'harolds_apprentice' }, + ], + repeatable: false, + }, + { + id: 'bigmouth_riddles', + name: "Big Mouth's Riddle Chain", + emoji: '❓', + category: 'side', + description: 'McGee has a series of increasingly absurd riddles. Solve them all for a prize.', + giverNpc: 'bigmouth', + giverLocation: 'quackatoa', + levelRequired: 2, + prerequisiteQuestIds: [], + steps: [ + { + id: 'riddle_start', + description: 'Accept the riddle challenge', + objectives: [ + { id: 'talk_mcgee', description: 'Talk to McGee', type: 'talkToNpc', target: 'bigmouth', count: 1 }, + ], + dialogueOnStart: + '*McGee rises from the lava.* "THREE RIDDLES! Answer them through the encounters of Quackatoa and the prize is YOURS!"', + }, + { + id: 'riddles', + description: "Solve McGee's 3 riddles at Quackatoa", + objectives: [ + { + id: 'quack_encounters', + description: "Solve McGee's riddles at Quackatoa", + type: 'completeEncounter', + target: 'quackatoa', + count: 3, + }, + ], + dialogueOnComplete: + "*McGee's jaw drops (literally — he's a pelican).* \"You solved them ALL?! I... I don't have a fourth riddle prepared. This has never happened before. Here. Take this. You've earned it.\"", + }, + ], + rewards: [ + { type: 'xp', value: 75 }, + { type: 'breadcrumbs', value: 30 }, + { type: 'item', value: 'shieldToken', quantity: 1 }, + ], + repeatable: false, + }, + { + id: 'attenbird_documentary', + name: 'The Attenbird Documentary', + emoji: '🎬', + category: 'side', + description: 'Sir David Attenbird wants to film your adventures. Visit 3 locations while he narrates.', + giverNpc: 'attenbird', + giverLocation: 'coliseum', + levelRequired: 4, + prerequisiteQuestIds: [], + steps: [ + { + id: 'casting', + description: 'Accept the documentary role', + objectives: [ + { id: 'talk_attenbird', description: 'Talk to Attenbird', type: 'talkToNpc', target: 'attenbird', count: 1 }, + ], + dialogueOnStart: + '*Attenbird adjusts his monocle.* "I need a STAR. Someone with charisma, danger, and a complete disregard for personal safety. ...You\'ll do."', + }, + { + id: 'filming', + description: 'Visit 3 different locations for filming', + objectives: [ + { id: 'film_1', description: 'Visit the Puddle', type: 'visitLocation', target: 'puddle', count: 1 }, + { id: 'film_2', description: 'Visit Quackatoa', type: 'visitLocation', target: 'quackatoa', count: 1 }, + { id: 'film_3', description: 'Visit the Frozen Pond', type: 'visitLocation', target: 'frozenPond', count: 1 }, + ], + }, + { + id: 'premiere', + description: 'Return to the Coliseum for the premiere', + objectives: [ + { + id: 'return_premiere', + description: 'Return to the Coliseum', + type: 'visitLocation', + target: 'coliseum', + count: 1, + }, + { id: 'talk_premiere', description: 'Talk to Attenbird', type: 'talkToNpc', target: 'attenbird', count: 1 }, + ], + dialogueOnComplete: + '*The documentary premieres to a crowd of ten thousand ducks. They cheer. They cry. They throw breadcrumbs. Attenbird weeps.* "MAGNIFICENT. You, my friend, are a star."', + }, + ], + rewards: [ + { type: 'xp', value: 75 }, + { type: 'breadcrumbs', value: 50 }, + { type: 'title', value: 'documentary_star' }, + ], + repeatable: false, + }, + { + id: 'frostbeak_memories', + name: "Frostbeak's Memories", + emoji: '🧊', + category: 'side', + description: "Help Frostbeak remember their past by visiting the places they've forgotten.", + giverNpc: 'frostbeak', + giverLocation: 'frozenPond', + levelRequired: 3, + prerequisiteQuestIds: [], + steps: [ + { + id: 'remember', + description: 'Speak with Frostbeak about their past', + objectives: [ + { id: 'talk_frost', description: 'Talk to Frostbeak', type: 'talkToNpc', target: 'frostbeak', count: 1 }, + ], + dialogueOnStart: + '*Frostbeak\'s eyes go distant.* "I remember... a warm place. And a high place. Before the ice. Can you visit them for me? Tell me what you see?"', + }, + { + id: 'visit_memories', + description: 'Visit the places Frostbeak remembers', + objectives: [ + { + id: 'warm_place', + description: 'Visit Quackatoa (the warm place)', + type: 'visitLocation', + target: 'quackatoa', + count: 1, + }, + { + id: 'high_place', + description: 'Visit the Highway (the high place)', + type: 'visitLocation', + target: 'highway', + count: 1, + }, + ], + dialogueOnComplete: + '*Frostbeak listens to your descriptions. The ice around them cracks a little more.* "I remember now. I remember the warmth. Thank you... for being my eyes."', + }, + ], + rewards: [ + { type: 'xp', value: 75 }, + { type: 'breadcrumbs', value: 30 }, + { type: 'item', value: 'hpPotion', quantity: 2 }, + ], + repeatable: false, + }, + { + id: 'chad_redemption', + name: "Chad's Redemption", + emoji: '🦅', + category: 'side', + description: + "Chad wants to go legit. Help him set up a breadcrumb stand. (Requires cutting Chad in during Trenchbill's quest.)", + giverNpc: 'chad', + giverLocation: 'puddle', + levelRequired: 5, + prerequisiteQuestIds: ['trenchbill_underworld'], + requiresChoiceMade: { questId: 'trenchbill_underworld', choiceLabel: 'Cut Chad in for a share' }, + steps: [ + { + id: 'legit', + description: 'Talk to Chad about going legitimate', + objectives: [{ id: 'talk_chad', description: 'Talk to Chad', type: 'talkToNpc', target: 'chad', count: 1 }], + dialogueOnStart: + '*Chad is unusually quiet.* "So... after that thing with Trenchbill... I\'ve been thinking. Maybe I should stop stealing. Maybe I should... SELL things. Like a REAL business duck."', + }, + { + id: 'supplies', + description: 'Help Chad gather supplies (explore the Puddle)', + objectives: [ + { + id: 'puddle_supply', + description: 'Complete 2 encounters at the Puddle', + type: 'completeEncounter', + target: 'puddle', + count: 2, + }, + ], + }, + { + id: 'grand_opening', + description: "Attend Chad's grand opening", + objectives: [ + { id: 'talk_chad_open', description: 'Talk to Chad', type: 'talkToNpc', target: 'chad', count: 1 }, + ], + dialogueOnComplete: + '*Chad stands behind a cardboard box with "CHAD\'S BREADCRUMBS" scrawled on it.* "WELCOME TO MY LEGITIMATE BUSINESS! ...Why are you crying? STOP CRYING! This is a PROFESSIONAL establishment!"', + }, + ], + rewards: [ + { type: 'xp', value: 75 }, + { type: 'breadcrumbs', value: 40 }, + { type: 'title', value: 'chads_friend' }, + ], + repeatable: false, + }, +] + +// --- Daily Quests --- + +const DAILY_QUESTS: QuestDefinition[] = [ + { + id: 'daily_patrol', + name: 'Daily Patrol', + emoji: '🔍', + category: 'daily', + description: 'Complete 3 encounters anywhere in the Quackverse.', + giverNpc: 'duchess', + giverLocation: 'coliseum', + levelRequired: 1, + prerequisiteQuestIds: [], + steps: [ + { + id: 'patrol', + description: 'Explore and complete 3 encounters', + objectives: [{ id: 'encounters', description: 'Complete 3 encounters', type: 'completeEncounter', count: 3 }], + dialogueOnStart: + '*The Duchess pins a patrol badge to your chest.* "The Quackverse doesn\'t patrol itself, darling. Three encounters. Chop chop."', + dialogueOnComplete: + '*The Duchess nods approvingly.* "Adequate patrolling. The Coliseum is marginally safer thanks to your... efforts."', + }, + ], + rewards: [ + { type: 'xp', value: 40 }, + { type: 'breadcrumbs', value: 20 }, + ], + repeatable: true, + cooldownHours: 20, + }, + { + id: 'daily_tourist', + name: 'The Grand Tour', + emoji: '🗺️', + category: 'daily', + description: 'Visit 3 different locations across the Quackverse.', + giverNpc: 'attenbird', + giverLocation: 'coliseum', + levelRequired: 2, + prerequisiteQuestIds: [], + steps: [ + { + id: 'tour', + description: 'Travel to 3 different locations', + objectives: [ + { id: 'loc_1', description: 'Travel to a new biome', type: 'visitLocation', count: 1 }, + { id: 'loc_2', description: 'Explore a second habitat', type: 'visitLocation', count: 1 }, + { id: 'loc_3', description: 'Document a third location', type: 'visitLocation', count: 1 }, + ], + dialogueOnStart: + '*Sir David Attenbird adjusts his monocle.* "Today we document the habitats of the Quackverse. Three locations. I\'ll be narrating from a safe distance."', + dialogueOnComplete: + '*Attenbird wipes a single tear.* "Magnificent footage. Three biomes in a single day. This will be the season finale."', + }, + ], + rewards: [ + { type: 'xp', value: 40 }, + { type: 'breadcrumbs', value: 25 }, + ], + repeatable: true, + cooldownHours: 20, + }, + { + id: 'daily_fighter', + name: 'Arena Training', + emoji: '⚔️', + category: 'daily', + description: 'Participate in a tournament. Win or lose, the Arena respects the effort.', + giverNpc: 'duchess', + giverLocation: 'coliseum', + levelRequired: 2, + prerequisiteQuestIds: [], + steps: [ + { + id: 'fight', + description: 'Enter and complete a tournament', + objectives: [ + { + id: 'tournament', + description: 'Complete a tournament', + type: 'defeatInTournament', + count: 1, + }, + ], + dialogueOnStart: + '*The Arena announcer bellows:* "FRESH MEAT FOR THE QUACKTAMENT! Step into the ring and prove your worth — win or lose, the Arena respects those who fight!"', + dialogueOnComplete: + '*The crowd erupts.* "Another warrior tempered in the fires of combat! The Arena remembers your name."', + }, + ], + rewards: [ + { type: 'xp', value: 50 }, + { type: 'breadcrumbs', value: 30 }, + { type: 'item', value: 'hpPotion', quantity: 1 }, + ], + repeatable: true, + cooldownHours: 20, + }, +] + +// --- Quest Registry --- + +export const ALL_QUESTS: QuestDefinition[] = [...MAIN_QUESTS, ...SIDE_QUESTS, ...DAILY_QUESTS] + +export const getQuestById = (id: string): QuestDefinition | undefined => { + return ALL_QUESTS.find((q) => q.id === id) +} + +// --- Quest Engine --- + +export const getAvailableQuests = ( + playerLevel: number, + completedQuestIds: string[], + activeQuestIds: string[], + playerLocation: LocationId, + completedQuests?: CompletedQuest[] +): QuestDefinition[] => { + return ALL_QUESTS.filter((q) => { + if (q.levelRequired > playerLevel) { + return false + } + if (activeQuestIds.includes(q.id)) { + return false + } + if (!q.repeatable && completedQuestIds.includes(q.id)) { + return false + } + if (!q.prerequisiteQuestIds.every((preReq) => completedQuestIds.includes(preReq))) { + return false + } + if (q.requiresChoiceMade && completedQuests) { + const reqQuest = completedQuests.find((c) => c.questId === q.requiresChoiceMade!.questId) + if (!reqQuest || !reqQuest.choicesMade.includes(q.requiresChoiceMade.choiceLabel)) { + return false + } + } + // Only show quests whose giver is at the player's current location + if (q.giverLocation !== playerLocation) { + return false + } + return true + }) +} + +export const getAvailableQuestsFromNpc = ( + npcId: NpcId, + playerLevel: number, + completedQuestIds: string[], + activeQuestIds: string[], + completedQuests?: CompletedQuest[] +): QuestDefinition[] => { + return ALL_QUESTS.filter((q) => { + if (q.giverNpc !== npcId) { + return false + } + if (q.levelRequired > playerLevel) { + return false + } + if (activeQuestIds.includes(q.id)) { + return false + } + if (!q.repeatable && completedQuestIds.includes(q.id)) { + return false + } + if (!q.prerequisiteQuestIds.every((preReq) => completedQuestIds.includes(preReq))) { + return false + } + if (q.requiresChoiceMade && completedQuests) { + const reqQuest = completedQuests.find((c) => c.questId === q.requiresChoiceMade!.questId) + if (!reqQuest || !reqQuest.choicesMade.includes(q.requiresChoiceMade.choiceLabel)) { + return false + } + } + return true + }) +} + +export const getCurrentStep = (quest: QuestProgress, questDef: QuestDefinition): QuestStep | undefined => { + return questDef.steps.find((s) => s.id === quest.currentStepId) +} + +export const getStepIndex = (questDef: QuestDefinition, stepId: string): number => { + return questDef.steps.findIndex((s) => s.id === stepId) +} + +export const checkObjectiveProgress = ( + quest: QuestProgress, + questDef: QuestDefinition, + eventType: QuestObjectiveType, + eventTarget?: string +): { updated: boolean; objectiveCompleted?: string } => { + const step = getCurrentStep(quest, questDef) + if (!step) { + return { updated: false } + } + + for (const obj of step.objectives) { + if (obj.type !== eventType) { + continue + } + // Target matching: undefined target means "any" + if (obj.target && eventTarget && obj.target !== eventTarget) { + continue + } + const currentProgress = quest.objectiveProgress[obj.id] ?? 0 + if (currentProgress >= obj.count) { + continue + } + quest.objectiveProgress[obj.id] = currentProgress + 1 + if (quest.objectiveProgress[obj.id]! >= obj.count) { + return { updated: true, objectiveCompleted: obj.description } + } + return { updated: true } + } + return { updated: false } +} + +export const isStepComplete = (quest: QuestProgress, questDef: QuestDefinition): boolean => { + const step = getCurrentStep(quest, questDef) + if (!step) { + return false + } + // Steps with choices and no objectives are complete (choice resolves them) + if (step.objectives.length === 0 && step.choices && step.choices.length > 0) { + return true + } + return step.objectives.every((obj) => (quest.objectiveProgress[obj.id] ?? 0) >= obj.count) +} + +export const advanceToNextStep = ( + quest: QuestProgress, + questDef: QuestDefinition, + choiceStepId?: string +): { completed: boolean; nextStep?: QuestStep } => { + const currentIdx = getStepIndex(questDef, quest.currentStepId) + + // If step has choices and a choice was made, go to the choice's nextStepId + if (choiceStepId) { + if (choiceStepId === 'complete') { + return { completed: true } + } + const nextStep = questDef.steps.find((s) => s.id === choiceStepId) + if (nextStep) { + quest.currentStepId = nextStep.id + quest.objectiveProgress = {} + return { completed: false, nextStep } + } + } + + // Otherwise advance linearly + const nextIdx = currentIdx + 1 + if (nextIdx >= questDef.steps.length) { + return { completed: true } + } + + // Skip steps that have id matching 'complete' (terminal) + const nextStep = questDef.steps[nextIdx] + if (!nextStep || nextStep.id === 'complete') { + return { completed: true } + } + + quest.currentStepId = nextStep.id + quest.objectiveProgress = {} + return { completed: false, nextStep } +} + +// --- Quest Journal Formatting --- + +export const formatQuestJournal = ( + activeQuests: QuestProgress[], + completedQuests: CompletedQuest[], + playerLevel: number, + playerLocation: LocationId, + generatedQuests?: QuestDefinition[] +): string => { + const lines: string[] = ['**Your Quest Journal**', ''] + + const lookup = (id: string): QuestDefinition | undefined => + getQuestById(id) ?? generatedQuests?.find((g) => g.id === id) + + // Active quests + if (activeQuests.length > 0) { + lines.push('**Active Quests:**') + for (const aq of activeQuests) { + const def = lookup(aq.questId) + if (!def) { + continue + } + const stepIdx = getStepIndex(def, aq.currentStepId) + 1 + const isBounty = aq.questId.startsWith('bounty_') + const tag = isBounty ? '[BOUNTY]' : `[${def.category.toUpperCase()}]` + lines.push(` ${def.emoji} ${tag} **${def.name}** (Step ${stepIdx}/${def.steps.length})`) + const step = getCurrentStep(aq, def) + if (step) { + for (const obj of step.objectives) { + const progress = aq.objectiveProgress[obj.id] ?? 0 + const done = progress >= obj.count ? '✅' : '⬜' + lines.push(` ${done} ${obj.description} (${progress}/${obj.count})`) + } + } + } + lines.push('') + } + + // Available quests (at current location) + const completedIds = completedQuests.map((q) => q.questId) + const activeIds = activeQuests.map((q) => q.questId) + const available = getAvailableQuests(playerLevel, completedIds, activeIds, playerLocation, completedQuests) + if (available.length > 0) { + lines.push('**Available Here:**') + for (const q of available) { + lines.push(` ${q.emoji} ${q.name} (Lvl ${q.levelRequired}) — *${q.description.slice(0, 60)}...*`) + } + lines.push('') + } + + lines.push(`**Completed:** ${completedQuests.length} quests`) + return lines.join('\n') +} + +export const getQuestStepProgress = (quest: QuestProgress, questDef: QuestDefinition): string => { + const stepIdx = getStepIndex(questDef, quest.currentStepId) + 1 + return `${stepIdx}/${questDef.steps.length}` +} diff --git a/bots/quack-norris/src/lib/save-profile.ts b/bots/quack-norris/src/lib/save-profile.ts new file mode 100644 index 00000000000..ae6f7b2bce5 --- /dev/null +++ b/bots/quack-norris/src/lib/save-profile.ts @@ -0,0 +1,36 @@ +import { PlayersTable } from '../tables/Players' +import type { ProfileRow } from './command-context' + +/** Version conflict error — thrown when a concurrent write has occurred since the profile was loaded. */ +export class VersionConflictError extends Error { + public constructor( + public readonly discordUserId: string, + public readonly expectedVersion: number, + public readonly actualVersion: number + ) { + super(`Version conflict for ${discordUserId}: expected ${expectedVersion}, found ${actualVersion}`) + this.name = 'VersionConflictError' + } +} + +/** + * Save profile to DB with optimistic concurrency control. + * Checks that the DB version matches what we loaded before writing. + * Throws VersionConflictError if another write occurred since we loaded. + */ +export const saveProfile = async (profile: ProfileRow): Promise => { + const currentVersion = (profile.version as number) ?? 0 + + // Optimistic concurrency check: re-read to verify no concurrent write + const { rows } = await PlayersTable.findRows({ filter: { discordUserId: profile.discordUserId }, limit: 1 }) + const dbProfile = rows[0] + if (dbProfile) { + const dbVersion = (dbProfile.version as number) ?? 0 + if (dbVersion > currentVersion) { + throw new VersionConflictError(profile.discordUserId, currentVersion, dbVersion) + } + } + + const updated = { ...profile, version: currentVersion + 1 } + await PlayersTable.upsertRows({ rows: [updated], keyColumn: 'discordUserId' }) +} diff --git a/bots/quack-norris/src/lib/types.ts b/bots/quack-norris/src/lib/types.ts new file mode 100644 index 00000000000..e23fe896bbc --- /dev/null +++ b/bots/quack-norris/src/lib/types.ts @@ -0,0 +1,66 @@ +export type DuckClass = 'mallardNorris' | 'quackdini' | 'sirQuacksALot' | 'drQuackenstein' + +export type ActionType = 'light' | 'heavy' | 'block' | 'rest' | 'special' | 'forfeit' + +export type StatusEffectType = + | 'poison' + | 'inspired' + | 'exposed' + | 'shielded' + | 'decoy' + | 'resting' + | 'blessed' + | 'damageBoost' + | 'dodgeAll' + +export type StatusEffect = { + type: StatusEffectType + turnsLeft: number + stacks?: number + sourceId?: string +} + +export type GamePhase = 'registration' | 'classSelection' | 'combat' | 'finished' + +export type Player = { + discordUserId: string + name: string + hp: number + maxHp: number + energy: number + maxEnergy: number + alive: boolean + duckClass?: DuckClass + statusEffects: StatusEffect[] + specialCooldown: number + consecutiveTargetId?: string + consecutiveHits: number +} + +export type GameAction = { + discordUserId: string + type: ActionType + targetUserId?: string +} + +export type CombatEvent = { + text: string + type: 'attack' | 'block' | 'special' | 'status' | 'elimination' | 'chaos' | 'commentary' | 'rest' +} + +export type ClassDefinition = { + id: DuckClass + name: string + emoji: string + description: string + maxHp: number + maxEnergy: number + attackMod: number + defenseMod: number + passiveName: string + passiveDescription: string + specialName: string + specialDescription: string + specialCooldown: number + specialEnergyCost: number +} diff --git a/bots/quack-norris/src/tables/Actions.ts b/bots/quack-norris/src/tables/Actions.ts new file mode 100644 index 00000000000..5726153a433 --- /dev/null +++ b/bots/quack-norris/src/tables/Actions.ts @@ -0,0 +1,14 @@ +import { Table, z } from '@botpress/runtime' + +export const ActionsTable = new Table({ + name: 'ActionsTable', + keyColumn: 'actionKey', + columns: { + actionKey: z.string().describe('Compound key: gameId:round:discordUserId'), + gameId: z.string().describe('Game ID'), + round: z.number().describe('Round number'), + discordUserId: z.string().describe('Discord user ID of the player'), + actionType: z.enum(['light', 'heavy', 'block', 'rest', 'special', 'forfeit']).describe('Action type'), + targetUserId: z.string().optional().describe('Target Discord user ID'), + }, +}) diff --git a/bots/quack-norris/src/tables/Games.ts b/bots/quack-norris/src/tables/Games.ts new file mode 100644 index 00000000000..5b08c6830f7 --- /dev/null +++ b/bots/quack-norris/src/tables/Games.ts @@ -0,0 +1,68 @@ +import { Table, z } from '@botpress/runtime' + +export const GamesTable = new Table({ + name: 'GamesTable', + keyColumn: 'gameId', + columns: { + gameId: z.string().describe('Unique game identifier'), + channelId: z.string().describe('Discord channel where the game runs'), + pollMessageId: z.string().optional().describe('Registration poll message ID'), + phase: z.enum(['registration', 'classSelection', 'combat', 'finished']).describe('Current game phase'), + round: z.number().default(0).describe('Current combat round'), + quackeningAppliedRound: z.number().default(0).describe('Latest round where Quackening damage was applied'), + chaosEvent: z.string().optional().describe('Active chaos event name for this round'), + players: z + .array( + z.object({ + discordUserId: z.string(), + name: z.string(), + hp: z.number().default(100), + maxHp: z.number().default(100), + energy: z.number().default(100), + maxEnergy: z.number().default(100), + alive: z.boolean().default(true), + duckClass: z + .enum(['mallardNorris', 'quackdini', 'sirQuacksALot', 'drQuackenstein']) + .optional() + .describe('Selected duck class'), + statusEffects: z + .array( + z.object({ + type: z.enum([ + 'poison', + 'inspired', + 'exposed', + 'shielded', + 'decoy', + 'resting', + 'blessed', + 'damageBoost', + 'dodgeAll', + ]), + turnsLeft: z.number(), + stacks: z.number().optional(), + sourceId: z.string().optional(), + }) + ) + .default([]) + .describe('Active status effects'), + specialCooldown: z.number().default(0).describe('Rounds until special ability is available'), + consecutiveTargetId: z.string().optional().describe('Last targeted player for combo tracking'), + consecutiveHits: z.number().default(0).describe('Consecutive hits on same target'), + }) + ) + .default([]) + .describe('Players in the game'), + actions: z + .array( + z.object({ + discordUserId: z.string(), + type: z.enum(['light', 'heavy', 'block', 'rest', 'special', 'forfeit']), + targetUserId: z.string().optional(), + }) + ) + .default([]) + .describe('Actions submitted for the current round'), + winnerId: z.string().optional().describe('Discord user ID of the winner'), + }, +}) diff --git a/bots/quack-norris/src/tables/Players.ts b/bots/quack-norris/src/tables/Players.ts new file mode 100644 index 00000000000..02842abdf68 --- /dev/null +++ b/bots/quack-norris/src/tables/Players.ts @@ -0,0 +1,98 @@ +import { Table, z } from '@botpress/runtime' + +export const PlayersTable = new Table({ + name: 'PlayersTable', + keyColumn: 'discordUserId', + columns: { + discordUserId: z.string().describe('Discord user ID'), + displayName: z.string().describe('Player display name'), + guildId: z.string().optional().describe('Primary guild ID'), + adventureActive: z.boolean().default(false).describe('Whether the player has an active adventure'), + totalWins: z.number().default(0).describe('Total tournament wins'), + totalLosses: z.number().default(0).describe('Total tournament losses'), + totalKills: z.number().default(0).describe('Lifetime eliminations'), + currentLocation: z.string().default('coliseum').describe('Current adventure location ID'), + level: z.number().default(1).describe('Player level (1-10)'), + xp: z.number().default(0).describe('Total experience points'), + breadcrumbs: z.number().default(0).describe('Breadcrumb currency'), + title: z.string().default('Fledgling').describe('Active display title'), + titlesUnlocked: z.array(z.string()).default(['Fledgling']).describe('All unlocked titles'), + unlockedLocations: z + .array(z.string()) + .default(['coliseum', 'puddle', 'highway', 'quackatoa', 'parkBench', 'frozenPond']) + .describe('Accessible location IDs'), + inventory: z + .array( + z.object({ + itemId: z.string(), + name: z.string(), + type: z.enum([ + 'hpPotion', + 'energyDrink', + 'shieldToken', + 'damageBoost', + 'mirrorShard', + 'quackGrenade', + 'breadcrumbMagnet', + 'fogBomb', + ]), + quantity: z.number().default(1), + }) + ) + .default([]) + .describe('Player inventory (max 6 slots)'), + adventureState: z + .object({ + activeEncounterId: z.string().optional(), + encounterStep: z.number().default(0), + encountersCompleted: z.array(z.string()).default([]), + lastExploreAt: z.string().optional(), + currentNpc: z.string().optional(), + awaitingChoice: z.enum(['encounter', 'travel', 'quest_choice', 'quest_accept', 'shop', 'none']).default('none'), + pendingQuestId: z.string().optional(), + pendingNpcId: z.string().optional(), + visitedGatedLocations: z.array(z.string()).default([]), + lastEncounterResetAt: z.string().optional(), + lastMilestoneIndex: z.number().default(-1), + breadcrumbBoostActive: z.boolean().default(false), + }) + .default({ + encounterStep: 0, + encountersCompleted: [], + awaitingChoice: 'none', + visitedGatedLocations: [], + lastMilestoneIndex: -1, + breadcrumbBoostActive: false, + }) + .describe('Current adventure/encounter state'), + version: z.number().default(0).describe('Optimistic concurrency version counter'), + questState: z + .object({ + activeQuests: z + .array( + z.object({ + questId: z.string(), + currentStepId: z.string(), + objectiveProgress: z.record(z.string(), z.number()).default({}), + startedAt: z.string(), + choicesMade: z.array(z.string()).default([]), + }) + ) + .default([]), + completedQuests: z + .array( + z.object({ + questId: z.string(), + completedAt: z.string(), + choicesMade: z.array(z.string()).default([]), + }) + ) + .default([]), + dailyResetAt: z.string().optional(), + lastBountyCompletedAt: z.string().optional(), + generatedQuestsJson: z.string().default('[]'), + }) + .default({ activeQuests: [], completedQuests: [] }) + .describe('Quest progress and completion state'), + }, +}) diff --git a/bots/quack-norris/src/triggers/pollVote.ts b/bots/quack-norris/src/triggers/pollVote.ts new file mode 100644 index 00000000000..28f8b711329 --- /dev/null +++ b/bots/quack-norris/src/triggers/pollVote.ts @@ -0,0 +1,70 @@ +import { Trigger, actions } from '@botpress/runtime' +import type { Player } from '../lib/types' +import { GamesTable } from '../tables/Games' + +export const PollVote = new Trigger({ + name: 'pollVote', + description: 'Register players when they vote on the game registration poll', + events: ['discord:messagePollVoteAdd'], + + async handler({ event }) { + const { userId, messageId } = event.payload as { userId: string; messageId: string; guildId: string } + + let playerName = `Player_${userId.slice(-4)}` + try { + const member = await actions.discord.getGuildMember({ + guildId: (event.payload as { guildId: string }).guildId, + userId, + }) + playerName = + (member as { nick?: string; user: { username?: string } }).nick ?? + (member as { user: { username?: string } }).user.username ?? + playerName + } catch { + // fallback to default name + } + + const newPlayer = { + discordUserId: userId, + name: playerName, + hp: 100, + maxHp: 100, + energy: 100, + maxEnergy: 100, + alive: true, + statusEffects: [], + specialCooldown: 0, + consecutiveHits: 0, + } + const maxAttempts = 5 + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const { rows } = await GamesTable.findRows({ + filter: { pollMessageId: messageId, phase: 'registration' }, + limit: 1, + }) + const game = rows[0] + if (!game) { + return + } + + if (game.players.some((p: Player) => p.discordUserId === userId)) { + return + } + + const updatedPlayers = [...game.players, newPlayer] + await GamesTable.upsertRows({ rows: [{ ...game, players: updatedPlayers }], keyColumn: 'gameId' }) + + // Re-read to verify this vote was not lost by a concurrent write. + const { rows: verifyRows } = await GamesTable.findRows({ filter: { gameId: game.gameId }, limit: 1 }) + const verified = verifyRows[0] + if (!verified) { + return + } + if (verified.players.some((p: Player) => p.discordUserId === userId)) { + return + } + } + + console.warn('[pollVote] Failed to register player after retries', { messageId, userId }) + }, +}) diff --git a/bots/quack-norris/src/workflows/gameLoop.ts b/bots/quack-norris/src/workflows/gameLoop.ts new file mode 100644 index 00000000000..07137fac93c --- /dev/null +++ b/bots/quack-norris/src/workflows/gameLoop.ts @@ -0,0 +1,617 @@ +import { Workflow, z, actions } from '@botpress/runtime' +import { shouldTriggerChaos, rollChaosEvent, formatChaosAnnouncement } from '../lib/chaosEvents' +import { DUCK_CLASSES } from '../lib/classes' +import { ITEMS, addItemToInventory } from '../lib/items' +import { LOCATIONS, type LocationId } from '../lib/locations' +import { + narrateOpeningCeremony, + narrateRoundStart, + narrateVictory, + narrateDraw, + renderPlayerStatus, +} from '../lib/narration' +import { parseQuestState } from '../lib/profile' +import { + awardXp, + renderLevelUp, + XP_AWARDS, + BREADCRUMB_AWARDS, + checkTitleUnlocks, + getTitleName, + checkMilestones, +} from '../lib/progression' +import { getQuestById, checkObjectiveProgress, isStepComplete, advanceToNextStep } from '../lib/quests' +import type { Player } from '../lib/types' + +const MAX_ROUNDS = 15 +const ROUND_TIMEOUT_MS = 60_000 +const QUACKENING_DAMAGE = 5 + +type GameProfile = { + displayName: string + xp: number + level: number + breadcrumbs: number + titlesUnlocked: string[] + unlockedLocations: string[] + inventory: Parameters[0] +} + +const processQuestRewards = ( + profile: GameProfile, + def: { rewards: { type: string; value: unknown }[] }, + announcements: string[] +): void => { + let questXpGain = 0 + let questBcGain = 0 + for (const reward of def.rewards) { + if (!reward || reward.value == null) { + continue + } + if (reward.type === 'xp') { + questXpGain += reward.value as number + } else if (reward.type === 'breadcrumbs') { + questBcGain += reward.value as number + } else if (reward.type === 'title') { + const titleId = reward.value as string + if (!profile.titlesUnlocked.includes(titleId)) { + profile.titlesUnlocked = [...profile.titlesUnlocked, titleId] + announcements.push(`🏆 **${profile.displayName}** earned title: **${getTitleName(titleId)}**`) + } + } else if (reward.type === 'locationUnlock') { + const locId = reward.value as string + if (!profile.unlockedLocations.includes(locId)) { + profile.unlockedLocations = [...profile.unlockedLocations, locId] + const loc = LOCATIONS[locId as LocationId] + announcements.push(`🔓 **${profile.displayName}** unlocked: **${loc?.emoji ?? ''} ${loc?.name ?? locId}**`) + } + } else if (reward.type === 'item') { + addItemToInventory(profile.inventory, reward.value as Parameters[1]) + const itemDef = ITEMS[reward.value as keyof typeof ITEMS] + if (itemDef) { + announcements.push(`📦 **${profile.displayName}** received: ${itemDef.emoji} ${itemDef.name}`) + } + } + } + if (questXpGain > 0) { + const qXpResult = awardXp(profile.xp ?? 0, questXpGain) + profile.xp = qXpResult.newXp + profile.level = qXpResult.newLevel + if (qXpResult.leveledUp) { + const lvlText = renderLevelUp(qXpResult.oldLevel, qXpResult.newLevel, qXpResult.newXp).trim() + announcements.push(`${lvlText} (**${profile.displayName}**)`) + } + } + profile.breadcrumbs = (profile.breadcrumbs ?? 0) + questBcGain +} + +const QUICK_REFERENCE = [ + '*The Arena Scribe unrolls a scroll:*', + '`!light @target` ⚔️ 10e | `!heavy @target` 🔨 25e | `!block` 🛡️ 10e | `!special @target` 💥 | `!rest` 💤 +30e | `!use ` ✨ | `!forfeit` 🏳️', +].join('\n') + +export const GameLoop = new Workflow({ + name: 'gameLoop', + description: 'Main RPG game loop: class selection reveal, combat rounds with chaos events, dynamic narration', + timeout: '1h', + + input: z.object({ + gameId: z.string(), + channelId: z.string(), + conversationId: z.string(), + userId: z.string(), + }), + + state: z.object({ + currentRound: z.number().default(1), + gameOver: z.boolean().default(false), + }), + + output: z.object({ + winnerId: z.string().optional(), + totalRounds: z.number(), + }), + + async handler({ input, state, step, client }) { + const { GamesTable } = await import('../tables/Games') + + // --- Phase 1: Opening Ceremony + Class Reveal --- + await step('announce-start', async () => { + const { rows } = await GamesTable.findRows({ filter: { gameId: input.gameId }, limit: 1 }) + const game = rows[0] + if (!game) { + return + } + + const ceremony = narrateOpeningCeremony() + await client.createMessage({ + conversationId: input.conversationId, + userId: input.userId, + type: 'text', + tags: {}, + payload: { text: ceremony }, + }) + }) + + // Dramatic class reveal + await step('class-reveal', async () => { + const { rows } = await GamesTable.findRows({ filter: { gameId: input.gameId }, limit: 1 }) + const game = rows[0] + if (!game) { + return + } + + const reveals: string[] = ['**The fighters step into the arena...**\n'] + for (const player of game.players as Player[]) { + const classDef = player.duckClass ? DUCK_CLASSES[player.duckClass] : undefined + if (classDef) { + reveals.push(`${classDef.emoji} **${player.name}** enters as the **${classDef.name}**!`) + reveals.push(` *"${getClassEntrance(player.duckClass!)}"*`) + } else { + reveals.push(`❓ **${player.name}** enters... classless? Bold.`) + } + } + + reveals.push('') + reveals.push(QUICK_REFERENCE) + + await client.createMessage({ + conversationId: input.conversationId, + userId: input.userId, + type: 'text', + tags: {}, + payload: { text: reveals.join('\n') }, + }) + }) + + // --- Phase 2: Combat Loop --- + while (!state.gameOver && state.currentRound <= MAX_ROUNDS) { + const roundNum = state.currentRound + + // Chaos event: roll, store on game row, and announce (every 3 rounds) + if (shouldTriggerChaos(roundNum)) { + await step(`chaos-${roundNum}`, async () => { + const chaosEvent = rollChaosEvent() + + // Store chaos event on game row so resolveRound reads it + const { rows } = await GamesTable.findRows({ filter: { gameId: input.gameId }, limit: 1 }) + const game = rows[0] + if (game) { + await GamesTable.upsertRows({ + rows: [{ ...game, chaosEvent: chaosEvent.name }], + keyColumn: 'gameId', + }) + } + + const announcement = formatChaosAnnouncement(chaosEvent) + await client.createMessage({ + conversationId: input.conversationId, + userId: input.userId, + type: 'text', + tags: {}, + payload: { text: announcement }, + }) + }) + } + + // Round announcement + await step(`round-${roundNum}-announce`, async () => { + const { rows } = await GamesTable.findRows({ filter: { gameId: input.gameId }, limit: 1 }) + const game = rows[0] + if (!game) { + return + } + + const alivePlayers = (game.players as Player[]).filter((p) => p.alive) + const isQuackening = alivePlayers.length <= 3 && roundNum > 3 + + let roundText = narrateRoundStart(roundNum) + roundText += '\n\n' + + for (const p of alivePlayers) { + roundText += `${renderPlayerStatus(p)}\n` + } + + if (isQuackening) { + roundText += '\n⚡ **THE QUACKENING** ⚡ — All fighters take 5 dmg per round! Specials recharge faster!' + } + + roundText += `\n\nSubmit your actions! (60s)\n${QUICK_REFERENCE}` + + await client.createMessage({ + conversationId: input.conversationId, + userId: input.userId, + type: 'text', + tags: {}, + payload: { text: roundText }, + }) + }) + + // Apply Quackening damage in a dedicated idempotent step. + await step(`round-${roundNum}-quackening`, async () => { + const { rows } = await GamesTable.findRows({ filter: { gameId: input.gameId }, limit: 1 }) + const game = rows[0] + if (!game) { + return + } + + const alivePlayers = (game.players as Player[]).filter((p) => p.alive) + const isQuackening = alivePlayers.length <= 3 && roundNum > 3 + if (!isQuackening) { + return + } + + if ((game.quackeningAppliedRound ?? 0) >= roundNum) { + return + } + + const players = [...game.players] as Player[] + for (const p of players.filter((pl: Player) => pl.alive)) { + p.hp = Math.max(0, p.hp - QUACKENING_DAMAGE) + if (p.hp <= 0) { + p.alive = false + } + if (p.specialCooldown > 0) { + p.specialCooldown = Math.max(0, p.specialCooldown - 1) + } + } + + await GamesTable.upsertRows({ + rows: [{ ...game, players, quackeningAppliedRound: roundNum }], + keyColumn: 'gameId', + }) + }) + + // Wait for player actions + await step.sleep(`round-${roundNum}-wait`, ROUND_TIMEOUT_MS) + + // Show typing indicator before resolution + await step(`round-${roundNum}-typing`, async () => { + try { + await actions.discord.startTypingIndicator({ conversationId: input.conversationId }) + } catch (e) { + console.error('[gameLoop] typing indicator failed:', e) + } + }) + + // Resolve round (with error recovery to avoid zombie games) + let result: { log: string[]; gameOver: boolean; winnerId?: string } + try { + result = await step(`round-${roundNum}-resolve`, async () => { + return await actions.resolveRound({ gameId: input.gameId }) + }) + } catch { + // Clean up the game state so it doesn't stay in limbo + await step(`round-${roundNum}-error-recovery`, async () => { + const { rows } = await GamesTable.findRows({ filter: { gameId: input.gameId }, limit: 1 }) + const game = rows[0] + if (game) { + await GamesTable.upsertRows({ rows: [{ ...game, phase: 'finished' }], keyColumn: 'gameId' }) + } + await client.createMessage({ + conversationId: input.conversationId, + userId: input.userId, + type: 'text', + tags: {}, + payload: { + text: + '*The Arena shudders. Cracks appear in the ancient stonework.* Something went wrong behind the scenes ' + + '— the Arena Scribe is investigating. This tournament has been cancelled.\n\n' + + 'Type `!startTournament` to summon a new Quacktament!', + }, + }) + }) + return { winnerId: undefined, totalRounds: roundNum } + } + + // Post round results + await step(`round-${roundNum}-results`, async () => { + const logText = + result.log.length > 0 ? result.log.join('\n') : 'No actions this round. The arena is eerily silent...' + + await client.createMessage({ + conversationId: input.conversationId, + userId: input.userId, + type: 'text', + tags: {}, + payload: { + text: `**Round ${roundNum} Results:**\n${logText}`, + }, + }) + }) + + // Check game over + if (result.gameOver) { + state.gameOver = true + + await step('announce-winner', async () => { + const { rows } = await GamesTable.findRows({ filter: { gameId: input.gameId }, limit: 1 }) + const game = rows[0] + const winner = game?.players.find((p: Player) => p.discordUserId === result.winnerId) + + let text: string + if (winner) { + text = narrateVictory(winner.name) + if (game) { + const stats = generatePostGameStats(game.players as Player[], winner) + text += `\n\n${stats}` + } + } else { + text = narrateDraw() + } + + await client.createMessage({ + conversationId: input.conversationId, + userId: input.userId, + type: 'text', + tags: {}, + payload: { text }, + }) + }) + + // Update PlayersTable with win/loss/XP/breadcrumb/quest stats + await step('update-player-stats', async () => { + const { PlayersTable } = await import('../tables/Players') + const { rows } = await GamesTable.findRows({ filter: { gameId: input.gameId }, limit: 1 }) + const game = rows[0] + if (!game) { + return + } + + const announcements: string[] = [] + + // Fetch all player profiles in parallel (allSettled for partial-failure resilience) + type ProfileEntry = { + player: Player + profile: Awaited>['rows'][0] | undefined + } + const profileResults = await Promise.allSettled( + (game.players as Player[]).map(async (player): Promise => { + const { rows: profileRows } = await PlayersTable.findRows({ + filter: { discordUserId: player.discordUserId }, + limit: 1, + }) + return { player, profile: profileRows[0] } + }) + ) + const profileEntries: ProfileEntry[] = profileResults + .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled') + .map((r) => r.value) + + const updatedProfiles: NonNullable[] = [] + + for (const { player, profile } of profileEntries) { + if (!profile) { + continue + } + + const isWinner = player.discordUserId === result.winnerId + const xpGain = isWinner ? XP_AWARDS.tournamentWin : XP_AWARDS.tournamentLoss + const bcGain = isWinner ? BREADCRUMB_AWARDS.tournamentWin : 0 + + profile.totalWins += isWinner ? 1 : 0 + profile.totalLosses += isWinner ? 0 : 1 + const tourneyXpResult = awardXp(profile.xp ?? 0, xpGain) + profile.xp = tourneyXpResult.newXp + profile.breadcrumbs = (profile.breadcrumbs ?? 0) + bcGain + profile.level = tourneyXpResult.newLevel + if (tourneyXpResult.leveledUp) { + const lvlMsg = renderLevelUp( + tourneyXpResult.oldLevel, + tourneyXpResult.newLevel, + tourneyXpResult.newXp + ).trim() + announcements.push(`${lvlMsg} (**${profile.displayName}**)`) + } + + // Advance quest objectives for tournament participation + const qs = parseQuestState(profile.questState) + if (qs.activeQuests.length > 0) { + for (const quest of qs.activeQuests) { + const def = getQuestById(quest.questId) + if (!def) { + continue + } + if (isWinner) { + checkObjectiveProgress(quest, def, 'winTournament') + } + checkObjectiveProgress(quest, def, 'defeatInTournament') + + if (!isStepComplete(quest, def)) { + continue + } + const stepDef = def.steps.find((s) => s.id === quest.currentStepId) + const hasChoices = stepDef?.choices && stepDef.choices.length > 0 + if (hasChoices) { + continue + } + const advance = advanceToNextStep(quest, def) + if (!advance.completed) { + continue + } + qs.activeQuests = qs.activeQuests.filter((q) => q.questId !== quest.questId) + qs.completedQuests.push({ + questId: quest.questId, + completedAt: new Date().toISOString(), + choicesMade: quest.choicesMade, + }) + + if (!def.rewards || def.rewards.length === 0) { + announcements.push(`🎉 **${profile.displayName}** completed quest: **${def.name}**!`) + continue + } + processQuestRewards(profile as unknown as GameProfile, def, announcements) + announcements.push(`🎉 **${profile.displayName}** completed quest: **${def.name}**!`) + } + } + + profile.questState = qs as typeof profile.questState + + const newTitles = checkTitleUnlocks(profile as Parameters[0]) + for (const t of newTitles) { + if (!profile.titlesUnlocked.includes(t)) { + profile.titlesUnlocked = [...profile.titlesUnlocked, t] + announcements.push(`🏆 **${profile.displayName}** earned title: **${getTitleName(t)}**`) + } + } + + // Check milestone celebrations + const prevIdx = (profile.adventureState?.lastMilestoneIndex as number) ?? -1 + const milResult = checkMilestones(profile as Parameters[0], prevIdx) + if (milResult.messages.length > 0) { + for (const m of milResult.messages) { + announcements.push(`**${profile.displayName}** — ${m}`) + } + profile.adventureState = { + ...profile.adventureState, + lastMilestoneIndex: milResult.newIndex, + } + } + + profile.version = ((profile.version as number) ?? 0) + 1 + updatedProfiles.push(profile) + } + + // Batch write all updated profiles in a single call + if (updatedProfiles.length > 0) { + await PlayersTable.upsertRows({ + rows: updatedProfiles, + keyColumn: 'discordUserId', + }) + } + + // Post quest/title announcements + if (announcements.length > 0) { + await client.createMessage({ + conversationId: input.conversationId, + userId: input.userId, + type: 'text', + tags: {}, + payload: { text: announcements.join('\n') }, + }) + } + }) + + // Pin the winner announcement + await step('pin-winner', async () => { + try { + // Unpin old tournament results first + const { messages: pins } = await actions.discord.getChannelPins({ channelId: input.channelId }) + for (const pin of (pins as { id: string; content: string }[]).slice(0, 5)) { + if (pin.content.includes('VICTORY') || pin.content.includes('CHAMPION') || pin.content.includes('DRAW')) { + await actions.discord.unpinMessage({ channelId: input.channelId, messageId: pin.id }) + } + } + } catch (e) { + console.error('[gameLoop] pin cleanup failed:', e) + } + }) + + await step('post-game-prompt', async () => { + await client.createMessage({ + conversationId: input.conversationId, + userId: input.userId, + type: 'text', + tags: {}, + payload: { + text: 'The Arena of Mallard Destiny falls silent... for now. Type `!startTournament` to summon a new Quacktament!', + }, + }) + }) + + return { winnerId: result.winnerId, totalRounds: roundNum } + } + + state.currentRound = roundNum + 1 + } + + // MAX_ROUNDS reached without a winner — announce a draw + if (!state.gameOver) { + await step('max-rounds-draw', async () => { + const { rows } = await GamesTable.findRows({ filter: { gameId: input.gameId }, limit: 1 }) + const game = rows[0] + if (game) { + await GamesTable.upsertRows({ rows: [{ ...game, phase: 'finished' }], keyColumn: 'gameId' }) + } + + const drawText = narrateDraw() + await client.createMessage({ + conversationId: input.conversationId, + userId: input.userId, + type: 'text', + tags: {}, + payload: { + text: `${drawText}\n\n*${MAX_ROUNDS} rounds have passed. The Arena demands closure. Type \`!startTournament\` for a new fight!*`, + }, + }) + }) + } + + return { winnerId: undefined, totalRounds: state.currentRound } + }, +}) + +// --- Helper Functions --- + +const getClassEntrance = (duckClass: string): string => { + const entrances: Record = { + mallardNorris: [ + 'If it quacks like a problem, I punch it like a problem.', + "I didn't come here to make friends. I came here to make craters.", + 'The only thing getting blocked today is your escape route.', + ], + quackdini: [ + 'You never see the duck that gets you.', + "Now you see me... actually, that's already too late.", + "I'd tell you my strategy, but then I'd have to misdirect you.", + ], + sirQuacksALot: [ + "By the Great Mallard's light, I shall protect the righteous!", + 'My shield has never faltered. My resolve, even less so.', + 'I block, therefore I am. Also, +8 HP.', + ], + drQuackenstein: [ + 'The side effects of my potions have side effects. And those have side effects.', + "Everything is toxic if you add enough of it. That's science.", + "Don't worry. The burning sensation is completely normal. Probably.", + ], + } + + const classEntrances = entrances[duckClass] ?? ['*waddles in confidently*'] + return classEntrances[Math.floor(Math.random() * classEntrances.length)]! +} + +const generatePostGameStats = (players: Player[], winner: Player): string => { + const lines: string[] = ['**=== BATTLE REPORT ===**\n'] + + lines.push(`🏆 **Champion:** ${winner.name}`) + if (winner.duckClass) { + const classDef = DUCK_CLASSES[winner.duckClass] + lines.push(` Class: ${classDef.emoji} ${classDef.name}`) + } + lines.push(` Final HP: ${winner.hp}/${winner.maxHp}`) + lines.push('') + + lines.push('**Final Standings:**') + const sorted = [...players].sort((a, b) => { + if (a.alive && !b.alive) { + return -1 + } + if (!a.alive && b.alive) { + return 1 + } + return b.hp - a.hp + }) + + for (let i = 0; i < sorted.length; i++) { + const p = sorted[i]! + const classDef = p.duckClass ? DUCK_CLASSES[p.duckClass] : undefined + const classTag = classDef ? ` ${classDef.emoji}` : '' + const status = p.alive ? '👑' : '💀' + lines.push(`${status} #${i + 1} ${p.name}${classTag} — ${p.hp}/${p.maxHp} HP`) + } + + lines.push('\n*GG! Type `!startTournament` to start a new match!*') + + return lines.join('\n') +} diff --git a/bots/quack-norris/tsconfig.json b/bots/quack-norris/tsconfig.json new file mode 100644 index 00000000000..43c96a638cf --- /dev/null +++ b/bots/quack-norris/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist" + }, + "include": [".botpress/**/*", "src/**/*", "*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1394e443421..c4339a8ef30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -352,6 +352,22 @@ importers: specifier: 0.0.1 version: 0.0.1 + bots/quack-norris: + dependencies: + '@botpress/runtime': + specifier: ^1.16.6 + version: 1.16.6(debug@4.4.3)(esbuild@0.25.10)(typescript@5.9.3) + devDependencies: + '@botpress/adk': + specifier: ^1.16.6 + version: 1.16.6(@bpinternal/zui@1.3.3)(esbuild@0.25.10)(typescript@5.9.3) + '@botpress/adk-cli': + specifier: ^1.16.6 + version: 1.16.6(@bpinternal/zui@1.3.3)(esbuild@0.25.10)(typescript@5.9.3)(zod@3.25.76) + typescript: + specifier: ^5.6.3 + version: 5.9.3 + bots/sheetzy: dependencies: '@botpress/client': @@ -679,7 +695,7 @@ importers: version: link:../../packages/sdk openai: specifier: ^5.12.1 - version: 5.12.1(ws@8.19.0)(zod@3.24.2) + version: 5.12.1(ws@8.19.0)(zod@3.25.76) devDependencies: '@botpress/cli': specifier: workspace:* @@ -720,7 +736,7 @@ importers: version: 8.17.1 axios: specifier: 1.2.5 - version: 1.2.5 + version: 1.2.5(debug@4.4.3) chalk: specifier: ^4.1.2 version: 4.1.2 @@ -929,7 +945,7 @@ importers: version: link:../../packages/sdk openai: specifier: ^5.12.1 - version: 5.12.1(ws@8.19.0)(zod@3.24.2) + version: 5.12.1(ws@8.19.0)(zod@3.25.76) devDependencies: '@botpress/cli': specifier: workspace:* @@ -1071,7 +1087,7 @@ importers: version: link:../../packages/sdk '@google/genai': specifier: ^1.7.0 - version: 1.7.0 + version: 1.7.0(@modelcontextprotocol/sdk@1.27.1(zod@3.24.2)) devDependencies: '@botpress/cli': specifier: workspace:* @@ -1176,7 +1192,7 @@ importers: version: link:../../packages/sdk openai: specifier: ^5.12.1 - version: 5.12.1(ws@8.19.0)(zod@3.24.2) + version: 5.12.1(ws@8.19.0)(zod@3.25.76) devDependencies: '@botpress/cli': specifier: workspace:* @@ -1589,7 +1605,7 @@ importers: version: link:../../packages/sdk openai: specifier: ^6.9.0 - version: 6.9.0(ws@8.19.0) + version: 6.9.0(ws@8.19.0)(zod@3.25.76) devDependencies: '@botpress/cli': specifier: workspace:* @@ -1636,7 +1652,7 @@ importers: version: 14.1.0 resend: specifier: ^4.6.0 - version: 4.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.6.0(react-dom@18.3.1(react@19.2.3))(react@19.2.3) svix: specifier: ^1.69.0 version: 1.69.0 @@ -1905,7 +1921,7 @@ importers: version: link:../../packages/sdk-addons '@doist/todoist-api-typescript': specifier: ^3.0.3 - version: 3.0.3(type-fest@4.41.0) + version: 3.0.3(type-fest@5.4.4) devDependencies: '@botpress/cli': specifier: workspace:* @@ -2161,10 +2177,10 @@ importers: version: 9.0.1 jest: specifier: ^29.5.0 - version: 29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + version: 29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3)) ts-jest: specifier: ^29.1.0 - version: 29.1.0(@babel/core@7.26.9)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.26.9))(jest@29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3) + version: 29.1.0(@babel/core@7.28.0)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.28.0))(jest@29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3)))(typescript@5.9.3) integrations/zendesk: dependencies: @@ -2427,7 +2443,7 @@ importers: dependencies: axios: specifier: 1.2.5 - version: 1.2.5 + version: 1.2.5(debug@4.4.3) browser-or-node: specifier: ^2.1.1 version: 2.1.1 @@ -2732,7 +2748,7 @@ importers: version: 4.17.21 tsup: specifier: ^8.0.2 - version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3))(typescript@5.8.3) + version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3))(typescript@5.9.3) packages/cognitive: dependencies: @@ -2778,7 +2794,7 @@ importers: version: 11.1.6 tsup: specifier: ^8.0.2 - version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3))(typescript@5.8.3) + version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3))(typescript@5.9.3) packages/common: dependencies: @@ -2796,7 +2812,7 @@ importers: version: 15.0.1 openai: specifier: ^6.9.0 - version: 6.9.0(ws@8.19.0) + version: 6.9.0(ws@8.19.0)(zod@3.25.76) posthog-node: specifier: 5.14.1 version: 5.14.1 @@ -2942,7 +2958,7 @@ importers: version: 1.2.1(patch_hash=0354139cbc5dbd66e1bc59167ff8e42d3a9a2169038a35f1b7b1b4b843e08a6c) tsup: specifier: ^8.0.2 - version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3))(typescript@5.8.3) + version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3))(typescript@5.9.3) tsx: specifier: ^4.19.2 version: 4.19.2 @@ -2973,7 +2989,7 @@ importers: version: 0.3.0(esbuild@0.25.10) tsup: specifier: ^8.0.2 - version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3))(typescript@5.8.3) + version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3))(typescript@5.9.3) packages/sdk-addons: dependencies: @@ -3006,7 +3022,7 @@ importers: version: 4.17.21 vitest: specifier: ^2 || ^3 || ^4 || ^5 - version: 2.1.8(@types/node@22.16.4)(jsdom@24.1.3)(msw@2.12.0(@types/node@22.16.4)(typescript@5.8.3)) + version: 2.1.8(@types/node@22.16.4)(jsdom@24.1.3)(msw@2.12.0(@types/node@22.16.4)(typescript@5.9.3)) devDependencies: '@botpress/cli': specifier: workspace:* @@ -3028,7 +3044,7 @@ importers: version: 9.3.5 tsup: specifier: ^8.0.2 - version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3))(typescript@5.8.3) + version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3))(typescript@5.9.3) packages/zai: dependencies: @@ -3089,13 +3105,13 @@ importers: version: 4.17.21 msw: specifier: ^2.12.0 - version: 2.12.0(@types/node@22.16.4)(typescript@5.8.3) + version: 2.12.0(@types/node@22.16.4)(typescript@5.9.3) size-limit: specifier: ^11.1.6 version: 11.1.6 tsup: specifier: ^8.0.2 - version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3))(typescript@5.8.3) + version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3))(typescript@5.9.3) packages/zui: devDependencies: @@ -3128,7 +3144,7 @@ importers: version: 4.17.21 tsup: specifier: ^8.0.2 - version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3))(typescript@5.8.3) + version: 8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3))(typescript@5.9.3) plugins/analytics: dependencies: @@ -3359,6 +3375,10 @@ importers: packages: + '@alcalzone/ansi-tokenize@0.2.5': + resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} + engines: {node: '>=18'} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -3377,6 +3397,10 @@ packages: resolution: {integrity: sha512-pRrmXMCwnmrkS3MLgAIW5dXRzeTv6GLjkjb4HmxNnvAKXN1Nfzp4KmGADBQvlVUcqi+a5D+hfGDLLnd5NnYxog==} engines: {node: '>= 16'} + '@apidevtools/json-schema-ref-parser@11.9.3': + resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} + engines: {node: '>= 16'} + '@apidevtools/swagger-methods@3.0.2': resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} @@ -3729,10 +3753,6 @@ packages: resolution: {integrity: sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==} engines: {node: '>=6.9.0'} - '@babel/generator@7.27.1': - resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.29.1': resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} @@ -3852,11 +3872,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.27.2': - resolution: {integrity: sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.29.0': resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} engines: {node: '>=6.0.0'} @@ -3971,10 +3986,6 @@ packages: resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} engines: {node: '>=6.9.0'} - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} - engines: {node: '>=6.9.0'} - '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -3983,10 +3994,6 @@ packages: resolution: {integrity: sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.27.1': - resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.0': resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} @@ -4010,13 +4017,72 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@botpress/adk-cli@1.16.6': + resolution: {integrity: sha512-3cDFzHDZx2M5kLyGSVMInzugkcl1oQ0baj7b9jJ79RuO8fzgv3X6nc7l5coRfedkVu4kAWB1S1t0WoR+pUXROw==} + engines: {bun: '>=1.3.9', node: '>=22.0.0'} + hasBin: true + peerDependencies: + typescript: '>=4.5.0' + + '@botpress/adk@1.16.6': + resolution: {integrity: sha512-j3gdwp2PlSPyFep3CLaCDS4sA5+tctxSvgWgmEITlaaJhqowQQgn3V/s/TG2mehnPXSwgETOX8wYGi2tZaWRNQ==} + engines: {bun: '>=1.3.9', node: '>=22.0.0'} + peerDependencies: + typescript: '>=4.5.0' + '@botpress/api@1.76.0': resolution: {integrity: sha512-SRKVM0ak9gJGB6TOlraMx5pLQS8Cm58xwBPOiilmuNzB9Cab1hpSJuK4F4ErDOeL2fFEcXAa01/92AA0t/JRmA==} + '@botpress/chat@0.5.5': + resolution: {integrity: sha512-KgKhDi5pwG0QVg9ULq/oW+5uzaA8Ht42KOPh42LuK96niomNlDzPbM+b3ix7zp16KLDGkzCOO7oo8HuouLfNmw==} + engines: {node: '>=18.0.0'} + + '@botpress/cli@5.6.1': + resolution: {integrity: sha512-lwxxikPcdDmHJicqOKFsU/0wuCs5GTQk2mVFONjAKEqf4OlGD1BzurstyRDI9UgIhOgb72F7PPElhTN993w+PQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + '@botpress/client@1.36.0': + resolution: {integrity: sha512-RT+lIcXmYoTKdOKZqwbZ+pEQh7t+6Ole30EZq8Lvnw55w3wM0XUEVybyV23BByd+2iqW8DQb+BVj9joZsJSGRA==} + engines: {node: '>=18.0.0'} + + '@botpress/cognitive@0.3.15': + resolution: {integrity: sha512-i3NB9epo08qhT5VHBDT6GGSDlzKI817vV/yxIlAwWPlc+WA9rtur2sLCkDsHT4nivPzNGcJcAXKnwvR6+tnZLQ==} + engines: {node: '>=18.0.0'} + + '@botpress/runtime@1.16.6': + resolution: {integrity: sha512-8Wahy2VXgII7WDmNOrFL3WXg2gGuHVfGqPlZo/TzjAVEqiP3zuTHSVxiCVDVmkVm+5o+Jo74HeS3CIDyuEJZmg==} + engines: {bun: '>=1.3.9', node: '>=22.0.0'} + peerDependencies: + typescript: '>=4.5.0' + + '@botpress/sdk@5.4.4': + resolution: {integrity: sha512-n/HrFjmwoFDRAKGssiWfSVN/BbO57Xu3LGnZ0pRcfDutawi1FIVOjjSQMh6aZocNV0Nv70trPrlz4JN4s1QjVQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@bpinternal/zui': ^1.3.3 + esbuild: ^0.16.12 + peerDependenciesMeta: + esbuild: + optional: true + + '@botpress/webchat-client@0.4.0': + resolution: {integrity: sha512-Bp7FlAEeWGpj3Nt87zpYz6tK/iGqU6zV2+v0/6M7jjCKXO702feKfiIOUrHPwUgReKzkVgDOHKE2y10Bj0cHrw==} + + '@botpress/zai@2.6.1': + resolution: {integrity: sha512-54ZOeJSbd6k//BTo0QPpyFEReu/nfi7vXuS5e2TFaVt4eSxJxm+2plGcSDs2xqmtNKgYf2z/VVmBhQAPtn+wsQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@bpinternal/thicktoken': ^1.0.0 + '@bpinternal/zui': ^1.3.3 + '@bpinternal/const@0.1.0': resolution: {integrity: sha512-iIQg9oYYXOt+LSK34oNhJVQTcgRdtLmLZirEUaE+R9hnmbKONA5reR2kTewxZmekGyxej+5RtDK9xrC/0hmeAw==} engines: {node: '>=16.0.0', pnpm: 8.6.2} + '@bpinternal/const@0.4.2': + resolution: {integrity: sha512-01q5QJaBKjr6QMzqGI1dFssYDJfWGb1rlu6F7DUD1cvzaHWrbY01zo6YKvl0OkJ//11Is37h8UrOzJ64p55MNA==} + '@bpinternal/depsynky@0.3.0': resolution: {integrity: sha512-bhiEPOZXhJvzNzkwne3iRA+3RHppO8rLpDLZR8ee6XS7ATiTv4H9nOY31q89a4Pd/CXK2gWjJUmsq+QT93r72g==} engines: {node: '>=16.0.0', pnpm: 8.6.2} @@ -4026,6 +4092,10 @@ packages: resolution: {integrity: sha512-vg1hE0bWVLKZKklXSqVxoRhMR2xVJ2VnL70tmvMvw24R5kL6DLAxyttHAdsZD9k7tLqx4Enbh14OUCyJYsHTYw==} hasBin: true + '@bpinternal/jex@1.2.4': + resolution: {integrity: sha512-TVyOeCEfNRbge8cIG4OiD2LQlbfnpJMb730HS8AYgk1lf5fNCu0liqxtZVumcxl2lz7pfwUAIKcy8CGvjDumnA==} + engines: {node: '>=16.0.0', pnpm: 8.6.2} + '@bpinternal/log4bot@0.0.23': resolution: {integrity: sha512-mxM3vsVkX0i/MXuWYosCvehk8MZsXAPt2K0muSgxxU1levbXGOOI3giaj8L43OS2lMcz873H/86B8l2ilb7c/Q==} engines: {node: '>=16.0.0', pnpm: 8.6.2} @@ -4073,9 +4143,17 @@ packages: resolution: {integrity: sha512-NN7mX/32OpbDn9z4AFphRvYFxwRNA8D4RTMy7jOvKRQKEC2O1xlcWiKAOChlfsbaooL5bMFwURnsJVudPE2/cw==} engines: {node: '>=16.0.0', pnpm: 8.6.2} + '@bpinternal/yargs-extra@0.0.21': + resolution: {integrity: sha512-IvG58kga/BpAu6f6hdbJqkxk7PBN1R9zHTQAbkO9jQaVw4qPmMcUW5PgL2gu5RLhmYq0/7Oy48XQJIP2WhJ3Yw==} + engines: {node: '>=16.0.0', pnpm: 8.6.2} + '@bpinternal/yargs-extra@0.0.3': resolution: {integrity: sha512-e/unlq0LX4CJUv1jGOv1UgwB/h2M0NCXnwD4lEw496GpkQikO668RS+BBlRhkqdGfZmvKDkXZZ96xJCn+i6Ymg==} + '@bpinternal/zui@1.3.3': + resolution: {integrity: sha512-nTpX/jzx/zXavPMuCOUmgHJVlV3O/8QM8B3GPdgCXSQfxO5yYuGR4kF/xg0x32Esm3svzvK0qMAr6s8MKdgNKA==} + engines: {node: '>=18.0.0'} + '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} @@ -4765,6 +4843,12 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@hubspot/api-client@13.1.0': resolution: {integrity: sha512-l+GIaMOFF0rQKMEs6+hTdaGhw0i4ym/LppKryt1d4xWakFlckJ/BFxmbjRZXX248fwRzKPQCw2y0FZ0ajCWePQ==} engines: {node: '>=18.0.0'} @@ -4835,6 +4919,14 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -5041,6 +5133,16 @@ packages: '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@mswjs/interceptors@0.40.0': resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} engines: {node: '>=18'} @@ -5187,6 +5289,66 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api-logs@0.208.0': + resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@2.6.0': + resolution: {integrity: sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.2.0': + resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.6.0': + resolution: {integrity: sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation-http@0.208.0': + resolution: {integrity: sha512-rhmK46DRWEbQQB77RxmVXGyjs6783crXCnFjYQj+4tDH/Kpv9Rbg3h2kaNyp5Vz2emF1f9HOQQvZoHzwMWOFZQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.208.0': + resolution: {integrity: sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.6.0': + resolution: {integrity: sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.6.0': + resolution: {integrity: sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.6.0': + resolution: {integrity: sha512-YhswtasmsbIGEFvLGvR9p/y3PVRTfFf+mgY8van4Ygpnv4sA3vooAjvh+qAn9PNWxs4/IwGGqiQS0PPsaRJ0vQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.40.0': + resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} + engines: {node: '>=14'} + '@oxlint/darwin-arm64@1.14.0': resolution: {integrity: sha512-rcTw0QWeOc6IeVp+Up7WtcwdS9l4j7TOq4tihF0Ud/fl+VUVdvDCPuZ9QTnLXJhwMXiyQRWdxRyI6XBwf80ncQ==} cpu: [arm64] @@ -5227,10 +5389,92 @@ packages: cpu: [x64] os: [win32] + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + '@parcel/watcher@2.1.0': resolution: {integrity: sha512-8s8yYjd19pDSsBpbkOHnT6Z2+UJSuLQx61pCFM0s5wSRvKCEMDjd/cHY3/GI1szHIWbpXpsJdg3V6ISGGx9xDw==} engines: {node: '>= 10.0.0'} + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -5239,6 +5483,9 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@posthog/core@1.23.4': + resolution: {integrity: sha512-gSM1gnIuw5UOBUOTz0IhCTH8jOHoFr5rzSDb5m7fn9ofLHvz3boZT1L1f+bcuk+mvzNJfrJ3ByVQGKmUQnKQ8g==} + '@posthog/core@1.6.0': resolution: {integrity: sha512-Tbh8UACwbb7jFdDC7wwXHtfNzO+4wKh3VbyMHmp2UBe6w1jliJixexTJNfkqdGZm+ht3M10mcKvGGPnoZ2zLBg==} @@ -5538,6 +5785,9 @@ packages: '@rushstack/ts-command-line@4.23.2': resolution: {integrity: sha512-JJ7XZX5K3ThBBva38aomgsPv1L7FV6XmSOcR6HtM7HDFZJkepqT65imw26h9ggGqMjsY0R9jcl30tzKcVj9aOQ==} + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} @@ -5630,6 +5880,10 @@ packages: resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} engines: {node: '>=6'} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@sinonjs/commons@3.0.0': resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} @@ -6056,6 +6310,9 @@ packages: resolution: {integrity: sha512-g7SP7beaxrjxLnW//vskra07a1jsJowqp07KMouxh4gCwaF+ItHbRZN8O+1dhJivBi3VdasT71BPyk+8wzEreQ==} engines: {node: '>=15'} + '@ts-morph/common@0.28.1': + resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} + '@tsconfig/node10@1.0.9': resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} @@ -6490,6 +6747,15 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -6507,6 +6773,10 @@ packages: adaptivecards@1.2.3: resolution: {integrity: sha512-amQ5OSW3OpIkrxVKLjxVBPk/T49yuOtnqs1z5ZPfZr0+OpTovzmiHbyoAGDIsu5SNYHwOZFp/3LGOnRaALFa/g==} + adm-zip@0.5.16: + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + engines: {node: '>=12.0'} + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -6579,6 +6849,10 @@ packages: resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} engines: {node: '>=18'} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -6587,6 +6861,10 @@ packages: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -6599,6 +6877,10 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -6684,6 +6966,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + auto-bind@5.0.1: + resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -6815,6 +7101,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base-64@0.1.0: resolution: {integrity: sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==} @@ -6860,6 +7150,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -6900,6 +7194,10 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -7053,6 +7351,10 @@ packages: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -7097,6 +7399,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + ci-info@3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} @@ -7104,6 +7410,9 @@ packages: cjs-module-lexer@1.2.2: resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -7112,10 +7421,18 @@ packages: resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} engines: {node: '>=6'} + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + cli-color@2.0.3: resolution: {integrity: sha512-OkoZnxyC4ERN3zLzZaY9Emb7f/MhBOIpePv0Ycok0fJYT+Ouo00UBEIwsVsr0yoow++n5YWlSUgST9GKhNHiRQ==} engines: {node: '>=0.10'} + cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -7124,10 +7441,18 @@ packages: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + clipboardy@4.0.0: + resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} + engines: {node: '>=18'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -7143,10 +7468,17 @@ packages: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + code-error-fragment@0.0.230: resolution: {integrity: sha512-cadkfKp6932H8UkhzE/gcUqhRMNf8jHzkAN7+5Myabswaghu4xABTgPHDCjW+dBAJxj/SpkTYokpzDqY4pCzQw==} engines: {node: '>= 4'} + code-excerpt@4.0.0: + resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + collect-v8-coverage@1.0.1: resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} @@ -7194,6 +7526,10 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -7218,6 +7554,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -7228,9 +7568,17 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + convert-to-spaces@2.0.1: + resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.4.2: resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} engines: {node: '>= 0.6'} @@ -7249,6 +7597,10 @@ packages: core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -7399,6 +7751,14 @@ packages: babel-plugin-macros: optional: true + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -7468,6 +7828,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -7653,6 +8017,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} + es5-ext@0.10.64: resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} engines: {node: '>=0.10'} @@ -7853,10 +8220,18 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + eventsource@2.0.2: resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} engines: {node: '>=12.0.0'} + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -7865,6 +8240,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} @@ -7880,10 +8259,20 @@ packages: exponential-backoff@3.1.1: resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} + express-rate-limit@8.3.1: + resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} @@ -7932,6 +8321,9 @@ packages: fast-uri@3.0.2: resolution: {integrity: sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==} + fast-xml-builder@1.1.3: + resolution: {integrity: sha512-1o60KoFw2+LWKQu3IdcfcFlGTW4dpqEWmjhYec6H82AYZU2TVBXep6tMl8Z1Y+wM+ZrzCwe3BZ9Vyd9N2rIvmg==} + fast-xml-parser@4.2.5: resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==} hasBin: true @@ -7940,6 +8332,10 @@ packages: resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} hasBin: true + fast-xml-parser@5.5.5: + resolution: {integrity: sha512-NLY+V5NNbdmiEszx9n14mZBseJTC50bRq1VHsaxOmR72JDuZt+5J1Co+dC/4JPnyq+WrIHNM69r0sqf7BMb3Mg==} + hasBin: true + fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} @@ -7957,6 +8353,15 @@ packages: picomatch: optional: true + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} @@ -7964,6 +8369,10 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -8000,6 +8409,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-process@2.0.0: resolution: {integrity: sha512-YUBQnteWGASJoEVVsOXy6XtKAY2O1FCsWnnvQ8y0YwgY1rZiKeVptnFvMu6RSELZAJOGklqseTnUGGs5D0bKmg==} hasBin: true @@ -8065,8 +8478,8 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} forever-agent@0.6.1: @@ -8112,6 +8525,9 @@ packages: resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} deprecated: 'ACTION REQUIRED: SWITCH TO v3 - v1 and v2 are VULNERABLE! v1 is DEPRECATED FOR OVER 2 YEARS! Use formidable@latest or try formidable-mini for fresh projects' + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -8120,6 +8536,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fromentries@1.3.2: resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==} @@ -8191,6 +8611,10 @@ packages: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + get-intrinsic@1.2.7: resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} engines: {node: '>= 0.4'} @@ -8231,6 +8655,10 @@ packages: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -8261,6 +8689,12 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -8427,6 +8861,14 @@ packages: resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} engines: {node: '>=8'} + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + + hono@4.12.8: + resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} + engines: {node: '>=16.9.0'} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -8460,6 +8902,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -8484,6 +8930,10 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -8497,6 +8947,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -8519,6 +8973,9 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} + import-in-the-middle@2.0.6: + resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} + import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} @@ -8539,6 +8996,10 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -8549,6 +9010,19 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ink@6.8.0: + resolution: {integrity: sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==} + engines: {node: '>=20'} + peerDependencies: + '@types/react': '>=19.0.0' + react: '>=19.0.0' + react-devtools-core: '>=6.1.2' + peerDependenciesMeta: + '@types/react': + optional: true + react-devtools-core: + optional: true + intercom-client@4.0.0: resolution: {integrity: sha512-xtAsO0wnyd46JNRP98/JK0BXIfWNTO/tUk+1op4rnZ+N/DSdhAaBw8ZaJD8bL8ePNhyAMl/Vd6h7kYI/h9riaw==} engines: {node: '>= v8.0.0'} @@ -8561,6 +9035,10 @@ packages: resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} engines: {node: '>=12.22.0'} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -8656,6 +9134,10 @@ packages: resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} engines: {node: '>=18'} + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + is-generator-fn@2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} @@ -8671,6 +9153,11 @@ packages: is-hexadecimal@1.0.4: resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + is-in-ci@2.0.0: + resolution: {integrity: sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==} + engines: {node: '>=20'} + hasBin: true + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -8716,6 +9203,9 @@ packages: is-promise@2.2.2: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -8744,6 +9234,10 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -8763,6 +9257,10 @@ packages: is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -8783,6 +9281,10 @@ packages: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} + is64bit@2.0.0: + resolution: {integrity: sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==} + engines: {node: '>=18'} + isarray@0.0.1: resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} @@ -8830,6 +9332,10 @@ packages: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + jest-changed-files@29.5.0: resolution: {integrity: sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -8969,6 +9475,9 @@ packages: jose@4.15.2: resolution: {integrity: sha512-IY73F228OXRl9ar3jJagh7Vnuhj/GzBunPiZP13K0lOl7Am9SoWW3kEzq3MCllJMTtZqHTiDXQvoRd4U95aU6A==} + jose@6.2.1: + resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -9057,6 +9566,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -9190,6 +9702,17 @@ packages: resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==} engines: {node: '>=18.0.0'} + llmz@0.0.54: + resolution: {integrity: sha512-Tn3Zi2Ox6lKMBCB4q6GX+BocFGd8Igc6hu1tPk4wGmRN/iDGqJ9ms1ygT7xGpkeDCRvBNcSeAXH7ATfyg9elCg==} + peerDependencies: + '@botpress/client': 1.35.0 + '@botpress/cognitive': 0.3.14 + '@bpinternal/thicktoken': ^2.0.0 + '@bpinternal/zui': ^1.3.3 + peerDependenciesMeta: + '@botpress/client': + optional: true + load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -9323,6 +9846,10 @@ packages: lru_map@0.3.3: resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==} + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} @@ -9450,12 +9977,20 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memoizee@0.4.15: resolution: {integrity: sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==} merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -9595,10 +10130,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -9630,6 +10173,10 @@ packages: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.0.8: resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} @@ -9659,6 +10206,10 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -9667,6 +10218,9 @@ packages: mnemonist@0.38.3: resolution: {integrity: sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==} + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + moment@2.29.4: resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} @@ -9727,6 +10281,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -9743,6 +10301,9 @@ packages: node-addon-api@3.2.1: resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -9812,6 +10373,10 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -9840,6 +10405,9 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} + object-sizeof@2.6.5: + resolution: {integrity: sha512-Mu3udRqIsKpneKjIEJ2U/s1KmEgpl+N6cEX1o+dDl2aZ+VW5piHqNgomqAk5YMsDoSkpcA8HnIKx1eqGTKzdfw==} + object.assign@4.1.7: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} @@ -9967,6 +10535,10 @@ packages: resolution: {integrity: sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ==} engines: {node: '>=20'} + p-limit@7.3.0: + resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} + engines: {node: '>=20'} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -9995,6 +10567,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-json@6.5.0: resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} engines: {node: '>=8'} @@ -10013,6 +10588,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + parse-srcset@1.0.2: resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} @@ -10046,10 +10625,21 @@ packages: resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} engines: {node: '>= 0.4.0'} + patch-console@2.0.0: + resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.1.3: + resolution: {integrity: sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -10072,12 +10662,19 @@ packages: resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} engines: {node: '>=16 || 14 >=14.17'} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -10176,6 +10773,10 @@ packages: resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -10227,6 +10828,15 @@ packages: resolution: {integrity: sha512-NbqZMCwHectzfeFOeLqes1fPg/V5bsKhrBfyH1qHEcDb4ZHcbDARcqLE6JDhwMDQKa4YHkInXHITYscMuPylFw==} engines: {node: '>=20'} + posthog-node@5.28.2: + resolution: {integrity: sha512-a+unFAKU8Vtez1DAEgCXB/KOZbroQZE+GvnSr9B35u3uMUxtyPO5ulgLJo8AUcZ4prhv6ia8R1Xjr4BrxPfdsA==} + engines: {node: ^20.20.0 || >=22.22.0} + peerDependencies: + rxjs: ^7.0.0 + peerDependenciesMeta: + rxjs: + optional: true + preact-render-to-string@6.5.13: resolution: {integrity: sha512-iGPd+hKPMFKsfpR2vL4kJ6ZPcFIoWZEcBf0Dpm3zOpdVvj77aY8RlLiQji5OMrngEyaxGogeakTb54uS2FvA6w==} peerDependencies: @@ -10257,15 +10867,23 @@ packages: engines: {node: '>=14'} hasBin: true - prettier@3.5.3: - resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} hasBin: true + pretty-bytes@7.1.0: + resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} + engines: {node: '>=20'} + pretty-format@29.5.0: resolution: {integrity: sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + prismjs@1.29.0: resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} engines: {node: '>=6'} @@ -10325,6 +10943,10 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + qs@6.5.3: resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} engines: {node: '>=0.6'} @@ -10354,6 +10976,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -10369,10 +10995,20 @@ packages: react-promise-suspense@0.3.4: resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} + react-reconciler@0.33.0: + resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^19.2.0 + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + readable-stream@1.1.14: resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==} @@ -10474,6 +11110,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -10507,6 +11147,10 @@ packages: responselike@1.0.2: resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} + restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -10535,6 +11179,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rrweb-cssom@0.7.1: resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} @@ -10599,6 +11247,9 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + scmp@2.1.0: resolution: {integrity: sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==} deprecated: Just use Node.js's crypto.timingSafeEqual() @@ -10638,14 +11289,27 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serve-static@1.16.2: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -10763,6 +11427,10 @@ packages: resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} engines: {node: '>=18'} + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} @@ -10871,6 +11539,10 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + engines: {node: '>=20'} + string.prototype.trim@1.2.10: resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} engines: {node: '>= 0.4'} @@ -10903,6 +11575,10 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -10922,6 +11598,10 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -10937,6 +11617,9 @@ packages: strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + strnum@2.2.0: + resolution: {integrity: sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==} + sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} @@ -10989,15 +11672,31 @@ packages: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} + system-architecture@0.1.0: + resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} + engines: {node: '>=18'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + tar-stream@1.6.2: resolution: {integrity: sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==} engines: {node: '>= 0.8.0'} + tar@7.5.11: + resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==} + engines: {node: '>=18'} + telegraf@4.16.3: resolution: {integrity: sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==} engines: {node: ^12.20.0 || >=14.13.1} hasBin: true + terminal-size@4.0.1: + resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==} + engines: {node: '>=18'} + test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -11031,6 +11730,10 @@ packages: resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tinypool@1.0.1: resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -11154,6 +11857,9 @@ packages: esbuild: optional: true + ts-morph@27.0.2: + resolution: {integrity: sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==} + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -11283,10 +11989,18 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-fest@5.4.4: + resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} + engines: {node: '>=20'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + type@2.7.2: resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==} @@ -11331,6 +12045,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -11346,6 +12065,10 @@ packages: resolution: {integrity: sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg==} hasBin: true + ulid@3.0.2: + resolution: {integrity: sha512-yu26mwteFYzBAot7KVMqFGCVpsF6g8wXfJzQUHvu1no3+rRRSFcSV2nKeYvNPLD2J4b08jYBDhHUjeH0ygIl9w==} + hasBin: true + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -11367,6 +12090,10 @@ packages: unfetch@4.2.0: resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -11697,6 +12424,10 @@ packages: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} + widest-line@6.0.0: + resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} + engines: {node: '>=20'} + winston-transport@4.9.0: resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} engines: {node: '>= 12.0.0'} @@ -11815,6 +12546,10 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -11855,11 +12590,23 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + zod-to-json-schema@3.24.6: resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} peerDependencies: zod: ^3.24.1 + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} @@ -11869,6 +12616,9 @@ packages: zod@3.24.2: resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zwitch@1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} @@ -11877,6 +12627,11 @@ packages: snapshots: + '@alcalzone/ansi-tokenize@0.2.5': + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.8 @@ -11896,6 +12651,12 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.1.0 + '@apidevtools/json-schema-ref-parser@11.9.3': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + js-yaml: 4.1.0 + '@apidevtools/swagger-methods@3.0.2': {} '@asamuzakjp/css-color@3.2.0': @@ -12895,14 +13656,6 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 - '@babel/generator@7.27.1': - dependencies: - '@babel/parser': 7.27.2 - '@babel/types': 7.27.1 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.30 - jsesc: 3.1.0 - '@babel/generator@7.29.1': dependencies: '@babel/parser': 7.29.0 @@ -12939,7 +13692,20 @@ snapshots: '@babel/helper-optimise-call-expression': 7.27.1 '@babel/helper-replace-supers': 7.27.1(@babel/core@7.26.9) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -12948,8 +13714,8 @@ snapshots: '@babel/helper-member-expression-to-functions@7.27.1': dependencies: - '@babel/traverse': 7.27.1 - '@babel/types': 7.27.1 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -12962,8 +13728,8 @@ snapshots: '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.27.1 - '@babel/types': 7.27.1 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -12988,7 +13754,16 @@ snapshots: '@babel/core': 7.26.9 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -13003,7 +13778,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.27.1 + '@babel/types': 7.29.0 '@babel/helper-plugin-utils@7.27.1': {} @@ -13012,14 +13787,23 @@ snapshots: '@babel/core': 7.26.9 '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.27.1 - '@babel/types': 7.27.1 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -13051,37 +13835,33 @@ snapshots: dependencies: '@babel/types': 7.26.9 - '@babel/parser@7.27.2': - dependencies: - '@babel/types': 7.27.1 - '@babel/parser@7.29.0': dependencies: '@babel/types': 7.29.0 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.9)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.9)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.9)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.9)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.9)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.26.9)': @@ -13089,39 +13869,44 @@ snapshots: '@babel/core': 7.26.9 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.9)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.9)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.9)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.9)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.9)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.9)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.9)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.0)': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.26.9)': @@ -13129,6 +13914,11 @@ snapshots: '@babel/core': 7.26.9 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.26.9)': dependencies: '@babel/core': 7.26.9 @@ -13137,6 +13927,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-transforms': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.9)': dependencies: '@babel/core': 7.26.9 @@ -13148,6 +13946,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) + '@babel/types': 7.26.9 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-typescript@7.27.1(@babel/core@7.26.9)': dependencies: '@babel/core': 7.26.9 @@ -13159,6 +13968,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-typescript@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.1 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + '@babel/preset-typescript@7.26.0(@babel/core@7.26.9)': dependencies: '@babel/core': 7.26.9 @@ -13170,6 +13990,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/preset-typescript@7.26.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + '@babel/runtime@7.24.7': dependencies: regenerator-runtime: 0.14.1 @@ -13182,12 +14013,6 @@ snapshots: '@babel/parser': 7.26.9 '@babel/types': 7.26.9 - '@babel/template@7.27.2': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.27.2 - '@babel/types': 7.27.1 - '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 @@ -13206,63 +14031,255 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/traverse@7.27.1': + '@babel/traverse@7.29.0': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.1 - '@babel/parser': 7.27.2 - '@babel/template': 7.27.2 - '@babel/types': 7.27.1 - debug: 4.4.1 - globals: 11.12.0 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.26.9': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@babel/types@7.27.1': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcherny/json-schema-ref-parser@10.0.5-fork': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + call-me-maybe: 1.0.2 + js-yaml: 4.1.0 + + '@bcoe/v8-coverage@0.2.3': {} + + '@botpress/adk-cli@1.16.6(@bpinternal/zui@1.3.3)(esbuild@0.25.10)(typescript@5.9.3)(zod@3.25.76)': + dependencies: + '@botpress/adk': 1.16.6(@bpinternal/zui@1.3.3)(esbuild@0.25.10)(typescript@5.9.3) + '@botpress/chat': 0.5.5(debug@4.4.3) + '@botpress/cli': 5.6.1(@bpinternal/zui@1.3.3)(debug@4.4.3) + '@botpress/client': 1.36.0(debug@4.4.3) + '@botpress/runtime': 1.16.6(debug@4.4.3)(esbuild@0.25.10)(typescript@5.9.3) + '@botpress/sdk': 5.4.4(@bpinternal/zui@1.3.3)(debug@4.4.3)(esbuild@0.25.10) + '@botpress/webchat-client': 0.4.0 + '@modelcontextprotocol/sdk': 1.27.1(zod@3.25.76) + adm-zip: 0.5.16 + chalk: 5.4.1 + clipboardy: 4.0.0 + commander: 14.0.3 + debug: 4.4.3 + execa: 9.6.1 + glob: 11.1.0 + highlight.js: 11.11.1 + ink: 6.8.0(react@19.2.3) + jsonc-parser: 3.3.1 + open: 10.2.0 + posthog-node: 5.28.2 + prettier: 3.8.1 + react: 19.2.3 + semver: 7.7.4 + tar: 7.5.11 + typescript: 5.9.3 transitivePeerDependencies: + - '@bpinternal/zui' + - '@cfworker/json-schema' + - '@types/react' + - babel-plugin-macros + - bufferutil + - encoding + - esbuild + - react-devtools-core + - rxjs - supports-color + - utf-8-validate + - zod + + '@botpress/adk@1.16.6(@bpinternal/zui@1.3.3)(esbuild@0.25.10)(typescript@5.9.3)': + dependencies: + '@botpress/chat': 0.5.5(debug@4.4.3) + '@botpress/cli': 5.6.1(@bpinternal/zui@1.3.3)(debug@4.4.3) + '@botpress/client': 1.36.0(debug@4.4.3) + '@botpress/cognitive': 0.3.15 + '@botpress/runtime': 1.16.6(debug@4.4.3)(esbuild@0.25.10)(typescript@5.9.3) + '@botpress/sdk': 5.4.4(@bpinternal/zui@1.3.3)(debug@4.4.3)(esbuild@0.25.10) + '@bpinternal/jex': 1.2.4 + '@bpinternal/yargs-extra': 0.0.21 + '@parcel/watcher': 2.5.6 + debug: 4.4.3 + dedent: 1.7.2 + execa: 9.6.1 + glob: 11.1.0 + luxon: 3.7.2 + prettier: 3.8.1 + semver: 7.7.2 + ts-morph: 27.0.2 + typescript: 5.9.3 + transitivePeerDependencies: + - '@bpinternal/zui' + - babel-plugin-macros + - bufferutil + - encoding + - esbuild + - supports-color + - utf-8-validate + + '@botpress/api@1.76.0': + dependencies: + '@bpinternal/opapi': 1.0.0(openapi-types@12.1.3) + transitivePeerDependencies: + - debug + - openapi-types + - supports-color + + '@botpress/chat@0.5.5(debug@4.4.3)': + dependencies: + axios: 1.2.5(debug@4.4.3) + browser-or-node: 2.1.1 + event-source-polyfill: 1.0.31 + eventsource: 2.0.2 + jsonwebtoken: 9.0.2 + qs: 6.13.0 + verror: 1.10.1 + zod: 3.25.76 + transitivePeerDependencies: + - debug + + '@botpress/cli@5.6.1(@bpinternal/zui@1.3.3)(debug@4.4.3)': + dependencies: + '@apidevtools/json-schema-ref-parser': 11.7.0 + '@botpress/chat': 0.5.5(debug@4.4.3) + '@botpress/client': 1.36.0(debug@4.4.3) + '@botpress/sdk': 5.4.4(@bpinternal/zui@1.3.3)(debug@4.4.3)(esbuild@0.25.10) + '@bpinternal/const': 0.1.0 + '@bpinternal/tunnel': 0.1.1 + '@bpinternal/verel': 0.2.0 + '@bpinternal/yargs-extra': 0.0.3 + '@parcel/watcher': 2.5.6 + '@stoplight/spectral-core': 1.19.1 + '@stoplight/spectral-functions': 1.9.0 + '@stoplight/spectral-parsers': 1.0.4 + '@types/lodash': 4.17.0 + '@types/verror': 1.10.6 + axios: 1.13.6(debug@4.4.3) + bluebird: 3.7.2 + boxen: 5.1.2 + chalk: 4.1.2 + dotenv: 16.4.4 + esbuild: 0.25.10 + handlebars: 4.7.8 + jsonpath-plus: 10.3.0 + latest-version: 5.1.0 + lodash: 4.17.21 + prettier: 3.8.1 + prompts: 2.4.2 + semver: 7.7.2 + uuid: 9.0.1 + verror: 1.10.1 + yn: 4.0.0 + transitivePeerDependencies: + - '@bpinternal/zui' + - bufferutil + - debug + - encoding + - utf-8-validate - '@babel/traverse@7.29.0': + '@botpress/client@1.36.0(debug@4.4.3)': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3 + axios: 1.13.6(debug@4.4.3) + axios-retry: 4.5.0(axios@1.13.6) + browser-or-node: 2.1.1 + qs: 6.13.0 transitivePeerDependencies: - - supports-color + - debug - '@babel/types@7.26.9': - dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 + '@botpress/cognitive@0.3.15': + dependencies: + exponential-backoff: 3.1.1 + nanoevents: 9.1.0 + + '@botpress/runtime@1.16.6(debug@4.4.3)(esbuild@0.25.10)(typescript@5.9.3)': + dependencies: + '@botpress/client': 1.36.0(debug@4.4.3) + '@botpress/cognitive': 0.3.15 + '@botpress/sdk': 5.4.4(@bpinternal/zui@1.3.3)(debug@4.4.3)(esbuild@0.25.10) + '@botpress/zai': 2.6.1(@bpinternal/thicktoken@2.0.0)(@bpinternal/zui@1.3.3) + '@bpinternal/const': 0.4.2 + '@bpinternal/thicktoken': 2.0.0 + '@bpinternal/zui': 1.3.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + axios: 1.13.6(debug@4.4.3) + bytes: 3.1.2 + dedent: 1.7.2 + fast-safe-stringify: 2.1.1 + fast-xml-parser: 5.5.5 + glob: 11.1.0 + llmz: 0.0.54(@botpress/client@1.36.0)(@botpress/cognitive@0.3.15)(@bpinternal/thicktoken@2.0.0)(@bpinternal/zui@1.3.3) + lodash: 4.17.21 + ms: 2.1.3 + object-sizeof: 2.6.5 + p-limit: 7.3.0 + pretty-bytes: 7.1.0 + typescript: 5.9.3 + ulid: 3.0.2 + undici: 7.16.0 + transitivePeerDependencies: + - babel-plugin-macros + - debug + - esbuild + - supports-color - '@babel/types@7.27.1': + '@botpress/sdk@5.4.4(@bpinternal/zui@1.3.3)(debug@4.4.3)(esbuild@0.25.10)': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@botpress/client': 1.36.0(debug@4.4.3) + '@bpinternal/zui': 1.3.3 + browser-or-node: 2.1.1 + semver: 7.7.2 + optionalDependencies: + esbuild: 0.25.10 + transitivePeerDependencies: + - debug - '@babel/types@7.29.0': + '@botpress/webchat-client@0.4.0': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + eventsource: 3.0.7 - '@bcherny/json-schema-ref-parser@10.0.5-fork': + '@botpress/zai@2.6.1(@bpinternal/thicktoken@2.0.0)(@bpinternal/zui@1.3.3)': dependencies: - '@jsdevtools/ono': 7.1.3 - '@types/json-schema': 7.0.15 - call-me-maybe: 1.0.2 - js-yaml: 4.1.0 + '@botpress/cognitive': 0.3.15 + '@bpinternal/thicktoken': 2.0.0 + '@bpinternal/zui': 1.3.3 + json5: 2.2.3 + jsonrepair: 3.10.0 + lodash-es: 4.17.21 + p-limit: 7.3.0 - '@bcoe/v8-coverage@0.2.3': {} + '@bpinternal/const@0.1.0': {} - '@botpress/api@1.76.0': + '@bpinternal/const@0.4.2': dependencies: - '@bpinternal/opapi': 1.0.0(openapi-types@12.1.3) - transitivePeerDependencies: - - debug - - openapi-types - - supports-color - - '@bpinternal/const@0.1.0': {} + zod: 3.25.76 '@bpinternal/depsynky@0.3.0': dependencies: @@ -13279,6 +14296,13 @@ snapshots: '@bpinternal/yargs-extra': 0.0.14 dotenv: 16.4.4 + '@bpinternal/jex@1.2.4': + dependencies: + '@apidevtools/json-schema-ref-parser': 11.9.3 + '@types/json-schema': 7.0.15 + lodash: 4.17.21 + node-fetch: 3.3.2 + '@bpinternal/log4bot@0.0.23': dependencies: chalk: 4.1.2 @@ -13380,6 +14404,15 @@ snapshots: yargs: 17.7.2 yn: 4.0.0 + '@bpinternal/yargs-extra@0.0.21': + dependencies: + '@types/yargs': 17.0.33 + decamelize: 5.0.1 + json-schema: 0.4.0 + lodash: 4.17.21 + yargs: 17.7.2 + yn: 4.0.0 + '@bpinternal/yargs-extra@0.0.3': dependencies: '@types/yargs': 17.0.24 @@ -13389,6 +14422,8 @@ snapshots: yargs: 17.7.2 yn: 4.0.0 + '@bpinternal/zui@1.3.3': {} + '@colors/colors@1.6.0': {} '@cspotcode/source-map-support@0.8.1': @@ -13428,14 +14463,14 @@ snapshots: '@devhigley/parse-proxy@1.0.3': {} - '@doist/todoist-api-typescript@3.0.3(type-fest@4.41.0)': + '@doist/todoist-api-typescript@3.0.3(type-fest@5.4.4)': dependencies: axios: 1.13.1 axios-case-converter: 1.1.1(axios@1.13.1) axios-retry: 3.9.1 runtypes: 6.7.0 ts-custom-error: 3.3.1 - type-fest: 4.41.0 + type-fest: 5.4.4 uuid: 9.0.1 transitivePeerDependencies: - debug @@ -13787,12 +14822,14 @@ snapshots: '@fastify/busboy@2.1.1': {} - '@google/genai@1.7.0': + '@google/genai@1.7.0(@modelcontextprotocol/sdk@1.27.1(zod@3.24.2))': dependencies: google-auth-library: 9.15.1 ws: 8.18.2 zod: 3.24.2 zod-to-json-schema: 3.24.6(zod@3.24.2) + optionalDependencies: + '@modelcontextprotocol/sdk': 1.27.1(zod@3.24.2) transitivePeerDependencies: - bufferutil - encoding @@ -13803,6 +14840,10 @@ snapshots: dependencies: graphql: 15.8.0 + '@hono/node-server@1.19.11(hono@4.12.8)': + dependencies: + hono: 4.12.8 + '@hubspot/api-client@13.1.0': dependencies: '@types/node': 22.16.4 @@ -13869,6 +14910,12 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/cliui@9.0.0': {} + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -13888,7 +14935,7 @@ snapshots: jest-util: 29.5.0 slash: 3.0.0 - '@jest/core@29.5.0(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3))': + '@jest/core@29.5.0(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3))': dependencies: '@jest/console': 29.5.0 '@jest/reporters': 29.5.0 @@ -13902,7 +14949,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.5.0 - jest-config: 29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + jest-config: 29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3)) jest-haste-map: 29.5.0 jest-message-util: 29.5.0 jest-regex-util: 29.4.3 @@ -13993,7 +15040,7 @@ snapshots: '@jest/source-map@29.4.3': dependencies: - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 callsites: 3.1.0 graceful-fs: 4.2.11 @@ -14013,7 +15060,7 @@ snapshots: '@jest/transform@29.5.0': dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@jest/types': 29.5.0 '@jridgewell/trace-mapping': 0.3.29 babel-plugin-istanbul: 6.1.1 @@ -14201,6 +15248,51 @@ snapshots: '@mixmark-io/domino@2.2.0': {} + '@modelcontextprotocol/sdk@1.27.1(zod@3.24.2)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.8) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.1(express@5.2.1) + hono: 4.12.8 + jose: 6.2.1 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.24.2 + zod-to-json-schema: 3.25.1(zod@3.24.2) + transitivePeerDependencies: + - supports-color + optional: true + + '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.8) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.1(express@5.2.1) + hono: 4.12.8 + jose: 6.2.1 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@mswjs/interceptors@0.40.0': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -14423,6 +15515,67 @@ snapshots: '@open-draft/until@2.1.0': {} + '@opentelemetry/api-logs@0.208.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/instrumentation-http@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + forwarded-parse: 2.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-trace-node@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/semantic-conventions@1.40.0': {} + '@oxlint/darwin-arm64@1.14.0': optional: true @@ -14447,6 +15600,45 @@ snapshots: '@oxlint/win32-x64@1.14.0': optional: true + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + '@parcel/watcher@2.1.0': dependencies: is-glob: 4.0.3 @@ -14454,11 +15646,36 @@ snapshots: node-addon-api: 3.2.1 node-gyp-build: 4.6.0 + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.3 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + '@pkgjs/parseargs@0.11.0': optional: true '@pkgr/core@0.2.9': {} + '@posthog/core@1.23.4': + dependencies: + cross-spawn: 7.0.6 + '@posthog/core@1.6.0': dependencies: cross-spawn: 7.0.6 @@ -14559,12 +15776,12 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-promise-suspense: 0.3.4 - '@react-email/render@1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-email/render@1.1.2(react-dom@18.3.1(react@19.2.3))(react@19.2.3)': dependencies: html-to-text: 9.0.5 - prettier: 3.5.3 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + prettier: 3.8.1 + react: 19.2.3 + react-dom: 18.3.1(react@19.2.3) react-promise-suspense: 0.3.4 '@react-email/row@0.0.10(react@18.3.1)': @@ -14731,6 +15948,8 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@sec-ant/readable-stream@0.4.1': {} + '@selderee/plugin-htmlparser2@0.11.0': dependencies: domhandler: 5.0.3 @@ -14835,6 +16054,8 @@ snapshots: '@sindresorhus/is@0.14.0': {} + '@sindresorhus/merge-streams@4.0.0': {} + '@sinonjs/commons@3.0.0': dependencies: type-detect: 4.0.8 @@ -14859,7 +16080,7 @@ snapshots: '@slack/types': 2.20.0 '@types/is-stream': 1.1.0 '@types/node': 22.16.4 - axios: 1.13.6 + axios: 1.13.6(debug@4.4.3) eventemitter3: 3.1.2 form-data: 2.5.1 is-electron: 2.2.2 @@ -15556,6 +16777,12 @@ snapshots: - encoding - supports-color + '@ts-morph/common@0.28.1': + dependencies: + minimatch: 10.2.4 + path-browserify: 1.0.1 + tinyglobby: 0.2.15 + '@tsconfig/node10@1.0.9': {} '@tsconfig/node12@1.0.11': {} @@ -15899,7 +17126,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.42.0(typescript@5.6.3) '@typescript-eslint/types': 8.42.0 - debug: 4.4.1 + debug: 4.4.3 typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -15992,13 +17219,13 @@ snapshots: msw: 2.12.0(@types/node@22.16.4)(typescript@5.6.3) vite: 5.4.10(@types/node@22.16.4) - '@vitest/mocker@2.1.8(msw@2.12.0(@types/node@22.16.4)(typescript@5.8.3))(vite@5.4.10(@types/node@22.16.4))': + '@vitest/mocker@2.1.8(msw@2.12.0(@types/node@22.16.4)(typescript@5.9.3))(vite@5.4.10(@types/node@22.16.4))': dependencies: '@vitest/spy': 2.1.8 estree-walker: 3.0.3 magic-string: 0.30.12 optionalDependencies: - msw: 2.12.0(@types/node@22.16.4)(typescript@5.8.3) + msw: 2.12.0(@types/node@22.16.4)(typescript@5.9.3) vite: 5.4.10(@types/node@22.16.4) '@vitest/pretty-format@2.1.4': @@ -16068,6 +17295,15 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -16078,9 +17314,11 @@ snapshots: adaptivecards@1.2.3: {} + adm-zip@0.5.16: {} + agent-base@6.0.2: dependencies: - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -16167,10 +17405,16 @@ snapshots: dependencies: environment: 1.1.0 + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + ansi-regex@5.0.1: {} ansi-regex@6.0.1: {} + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -16179,6 +17423,8 @@ snapshots: ansi-styles@6.2.1: {} + ansi-styles@6.2.3: {} + any-promise@1.3.0: {} anymatch@3.1.3: @@ -16280,6 +17526,8 @@ snapshots: asynckit@0.4.0: {} + auto-bind@5.0.1: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.0.0 @@ -16300,7 +17548,7 @@ snapshots: axios-error@1.0.4: dependencies: - axios: 0.21.4(debug@4.4.1) + axios: 0.21.4(debug@4.4.3) type-fest: 0.15.1 transitivePeerDependencies: - debug @@ -16310,6 +17558,11 @@ snapshots: '@babel/runtime': 7.24.7 is-retry-allowed: 2.2.0 + axios-retry@4.5.0(axios@1.13.6): + dependencies: + axios: 1.13.6(debug@4.4.3) + is-retry-allowed: 2.2.0 + axios-retry@4.5.0(axios@1.4.0): dependencies: axios: 1.4.0 @@ -16320,15 +17573,15 @@ snapshots: axios: 1.6.1 is-retry-allowed: 2.2.0 - axios@0.21.4(debug@4.4.1): + axios@0.21.4(debug@4.4.3): dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) transitivePeerDependencies: - debug axios@0.24.0: dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) transitivePeerDependencies: - debug @@ -16341,7 +17594,7 @@ snapshots: axios@1.11.0: dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16349,7 +17602,7 @@ snapshots: axios@1.12.2: dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16357,23 +17610,23 @@ snapshots: axios@1.13.1: dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - axios@1.13.6: + axios@1.13.6(debug@4.4.3): dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.15.11(debug@4.4.3) form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - axios@1.2.5: + axios@1.2.5(debug@4.4.3): dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16405,7 +17658,7 @@ snapshots: axios@1.6.2: dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16429,7 +17682,7 @@ snapshots: axios@1.6.8: dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16437,7 +17690,7 @@ snapshots: axios@1.7.2: dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16445,7 +17698,7 @@ snapshots: axios@1.7.4: dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16453,7 +17706,7 @@ snapshots: axios@1.7.7: dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16461,7 +17714,7 @@ snapshots: axios@1.7.8: dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16469,7 +17722,7 @@ snapshots: axios@1.7.9: dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) form-data: 4.0.2 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16477,7 +17730,7 @@ snapshots: axios@1.8.4: dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) form-data: 4.0.2 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16485,19 +17738,19 @@ snapshots: axios@1.9.0: dependencies: - follow-redirects: 1.15.6(debug@4.4.1) + follow-redirects: 1.15.6(debug@4.4.3) form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - babel-jest@29.5.0(@babel/core@7.26.9): + babel-jest@29.5.0(@babel/core@7.28.0): dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@jest/transform': 29.5.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.5.0(@babel/core@7.26.9) + babel-preset-jest: 29.5.0(@babel/core@7.28.0) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -16516,32 +17769,32 @@ snapshots: babel-plugin-jest-hoist@29.5.0: dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.27.1 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.6 - babel-preset-current-node-syntax@1.0.1(@babel/core@7.26.9): + babel-preset-current-node-syntax@1.0.1(@babel/core@7.28.0): dependencies: - '@babel/core': 7.26.9 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.9) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.9) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.9) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.9) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.26.9) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.9) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.9) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.9) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.9) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.26.9) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.9) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.9) - - babel-preset-jest@29.5.0(@babel/core@7.26.9): + '@babel/core': 7.28.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.0) + + babel-preset-jest@29.5.0(@babel/core@7.28.0): dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 babel-plugin-jest-hoist: 29.5.0 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.26.9) + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.28.0) bail@1.0.5: {} @@ -16549,6 +17802,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base-64@0.1.0: {} base64-js@1.5.1: {} @@ -16598,6 +17853,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.0 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} botbuilder-core@4.23.3: @@ -16657,7 +17926,7 @@ snapshots: '@azure/identity': 4.10.2 '@azure/msal-node': 2.16.3 '@types/jsonwebtoken': 9.0.6 - axios: 1.13.1 + axios: 1.13.6(debug@4.4.3) base64url: 3.0.1 botbuilder-stdlib: 4.23.3-internal botframework-schema: 4.23.3 @@ -16713,6 +17982,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -16875,6 +18148,8 @@ snapshots: chalk@5.4.1: {} + chalk@5.6.2: {} + char-regex@1.0.2: {} character-entities-html4@2.1.0: {} @@ -16932,14 +18207,20 @@ snapshots: dependencies: readdirp: 4.1.1 + chownr@3.0.0: {} + ci-info@3.8.0: {} cjs-module-lexer@1.2.2: {} + cjs-module-lexer@2.2.0: {} + clean-stack@2.2.0: {} cli-boxes@2.2.1: {} + cli-boxes@3.0.0: {} + cli-color@2.0.3: dependencies: d: 1.0.2 @@ -16948,6 +18229,10 @@ snapshots: memoizee: 0.4.15 timers-ext: 0.1.7 + cli-cursor@4.0.0: + dependencies: + restore-cursor: 4.0.0 + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -16957,8 +18242,19 @@ snapshots: slice-ansi: 5.0.0 string-width: 7.2.0 + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.0 + cli-width@4.1.0: {} + clipboardy@4.0.0: + dependencies: + execa: 8.0.1 + is-wsl: 3.1.0 + is64bit: 2.0.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -16973,8 +18269,14 @@ snapshots: co@4.6.0: {} + code-block-writer@13.0.3: {} + code-error-fragment@0.0.230: {} + code-excerpt@4.0.0: + dependencies: + convert-to-spaces: 2.0.1 + collect-v8-coverage@1.0.1: {} color-convert@1.9.3: @@ -17018,6 +18320,8 @@ snapshots: commander@13.1.0: {} + commander@14.0.3: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -17037,14 +18341,20 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.1: {} + content-type@1.0.5: {} convert-source-map@1.9.0: {} convert-source-map@2.0.0: {} + convert-to-spaces@2.0.1: {} + cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.4.2: {} cookie@0.7.1: {} @@ -17055,6 +18365,11 @@ snapshots: core-util-is@1.0.2: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + create-require@1.1.1: {} cross-fetch@4.1.0: @@ -17205,6 +18520,8 @@ snapshots: dedent@1.6.0: {} + dedent@1.7.2: {} + deep-eql@5.0.2: {} deep-extend@0.6.0: {} @@ -17252,6 +18569,8 @@ snapshots: destroy@1.2.0: {} + detect-libc@2.1.2: {} + detect-newline@3.1.0: {} devlop@1.1.0: @@ -17544,6 +18863,8 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 + es-toolkit@1.45.1: {} + es5-ext@0.10.64: dependencies: es6-iterator: 2.0.3 @@ -17874,8 +19195,14 @@ snapshots: eventemitter3@5.0.1: {} + eventsource-parser@3.0.6: {} + eventsource@2.0.2: {} + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -17900,6 +19227,21 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + exit@0.1.2: {} expect-type@1.1.0: {} @@ -17914,6 +19256,11 @@ snapshots: exponential-backoff@3.1.1: {} + express-rate-limit@8.3.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + express@4.21.2: dependencies: accepts: 1.3.8 @@ -17950,6 +19297,39 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + ext@1.7.0: dependencies: type: 2.7.2 @@ -17988,6 +19368,10 @@ snapshots: fast-uri@3.0.2: {} + fast-xml-builder@1.1.3: + dependencies: + path-expression-matcher: 1.1.3 + fast-xml-parser@4.2.5: dependencies: strnum: 1.0.5 @@ -17996,6 +19380,12 @@ snapshots: dependencies: strnum: 1.0.5 + fast-xml-parser@5.5.5: + dependencies: + fast-xml-builder: 1.1.3 + path-expression-matcher: 1.1.3 + strnum: 2.2.0 + fastq@1.15.0: dependencies: reusify: 1.0.4 @@ -18012,6 +19402,10 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + fecha@4.2.3: {} fetch-blob@3.2.0: @@ -18019,6 +19413,10 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -18053,6 +19451,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-process@2.0.0: dependencies: chalk: 4.1.2 @@ -18078,15 +19487,17 @@ snapshots: fn.name@1.1.0: {} - follow-redirects@1.15.11: {} + follow-redirects@1.15.11(debug@4.4.3): + optionalDependencies: + debug: 4.4.3 follow-redirects@1.15.2: {} follow-redirects@1.15.5: {} - follow-redirects@1.15.6(debug@4.4.1): + follow-redirects@1.15.6(debug@4.4.3): optionalDependencies: - debug: 4.4.1 + debug: 4.4.3 for-each@0.3.3: dependencies: @@ -18096,7 +19507,7 @@ snapshots: dependencies: is-callable: 1.2.7 - foreground-child@3.1.1: + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 @@ -18163,10 +19574,14 @@ snapshots: once: 1.4.0 qs: 6.13.0 + forwarded-parse@2.1.2: {} + forwarded@0.2.0: {} fresh@0.5.2: {} + fresh@2.0.0: {} + fromentries@1.3.2: {} fs-constants@1.0.0: {} @@ -18250,6 +19665,8 @@ snapshots: get-east-asian-width@1.3.0: {} + get-east-asian-width@1.5.0: {} + get-intrinsic@1.2.7: dependencies: call-bind-apply-helpers: 1.0.1 @@ -18302,6 +19719,11 @@ snapshots: get-stream@8.0.1: {} + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.3 @@ -18331,12 +19753,21 @@ snapshots: glob@10.3.10: dependencies: - foreground-child: 3.1.1 + foreground-child: 3.3.1 jackspeak: 2.3.6 minimatch: 9.0.5 minipass: 7.1.2 path-scurry: 1.10.2 + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.4 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -18579,6 +20010,10 @@ snapshots: hexoid@1.0.0: {} + highlight.js@11.11.1: {} + + hono@4.12.8: {} + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -18629,10 +20064,18 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -18645,14 +20088,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -18660,6 +20103,8 @@ snapshots: human-signals@5.0.0: {} + human-signals@8.0.1: {} + husky@9.1.7: {} iconv-lite@0.4.24: @@ -18670,6 +20115,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -18688,6 +20137,13 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-in-the-middle@2.0.6: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + import-lazy@4.0.0: {} import-local@3.1.0: @@ -18701,6 +20157,8 @@ snapshots: indent-string@4.0.0: {} + indent-string@5.0.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -18710,6 +20168,38 @@ snapshots: ini@1.3.8: {} + ink@6.8.0(react@19.2.3): + dependencies: + '@alcalzone/ansi-tokenize': 0.2.5 + ansi-escapes: 7.3.0 + ansi-styles: 6.2.1 + auto-bind: 5.0.1 + chalk: 5.6.2 + cli-boxes: 3.0.0 + cli-cursor: 4.0.0 + cli-truncate: 5.2.0 + code-excerpt: 4.0.0 + es-toolkit: 1.45.1 + indent-string: 5.0.0 + is-in-ci: 2.0.0 + patch-console: 2.0.0 + react: 19.2.3 + react-reconciler: 0.33.0(react@19.2.3) + scheduler: 0.27.0 + signal-exit: 3.0.7 + slice-ansi: 8.0.0 + stack-utils: 2.0.6 + string-width: 8.2.0 + terminal-size: 4.0.1 + type-fest: 5.4.4 + widest-line: 6.0.0 + wrap-ansi: 9.0.0 + ws: 8.19.0 + yoga-layout: 3.2.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + intercom-client@4.0.0: dependencies: axios: 0.24.0 @@ -18738,6 +20228,8 @@ snapshots: transitivePeerDependencies: - supports-color + ip-address@10.1.0: {} + ipaddr.js@1.9.1: {} is-alphabetical@1.0.4: {} @@ -18824,6 +20316,10 @@ snapshots: dependencies: get-east-asian-width: 1.3.0 + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.5.0 + is-generator-fn@2.1.0: {} is-generator-function@1.1.0: @@ -18839,6 +20335,8 @@ snapshots: is-hexadecimal@1.0.4: {} + is-in-ci@2.0.0: {} + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -18869,6 +20367,8 @@ snapshots: is-promise@2.2.2: {} + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.3 @@ -18890,6 +20390,8 @@ snapshots: is-stream@3.0.0: {} + is-stream@4.0.1: {} + is-string@1.1.1: dependencies: call-bound: 1.0.3 @@ -18911,6 +20413,8 @@ snapshots: is-typedarray@1.0.0: {} + is-unicode-supported@2.1.0: {} + is-weakmap@2.0.2: {} is-weakref@1.1.0: @@ -18930,6 +20434,10 @@ snapshots: dependencies: is-inside-container: 1.0.0 + is64bit@2.0.0: + dependencies: + system-architecture: 0.1.0 + isarray@0.0.1: {} isarray@1.0.0: {} @@ -18955,8 +20463,8 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.26.9 - '@babel/parser': 7.27.2 + '@babel/core': 7.28.0 + '@babel/parser': 7.29.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 semver: 6.3.1 @@ -18971,7 +20479,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.1 + debug: 4.4.3 istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: @@ -18988,6 +20496,10 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + jest-changed-files@29.5.0: dependencies: execa: 5.1.1 @@ -19018,16 +20530,16 @@ snapshots: transitivePeerDependencies: - supports-color - jest-cli@29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)): + jest-cli@29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3)): dependencies: - '@jest/core': 29.5.0(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + '@jest/core': 29.5.0(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3)) '@jest/test-result': 29.5.0 '@jest/types': 29.5.0 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 import-local: 3.1.0 - jest-config: 29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + jest-config: 29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3)) jest-util: 29.5.0 jest-validate: 29.5.0 prompts: 2.4.2 @@ -19037,12 +20549,12 @@ snapshots: - supports-color - ts-node - jest-config@29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)): + jest-config@29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3)): dependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@jest/test-sequencer': 29.5.0 '@jest/types': 29.5.0 - babel-jest: 29.5.0(@babel/core@7.26.9) + babel-jest: 29.5.0(@babel/core@7.28.0) chalk: 4.1.2 ci-info: 3.8.0 deepmerge: 4.3.1 @@ -19063,7 +20575,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.16.4 - ts-node: 10.9.2(@types/node@22.16.4)(typescript@5.8.3) + ts-node: 10.9.2(@types/node@22.16.4)(typescript@5.9.3) transitivePeerDependencies: - supports-color @@ -19223,18 +20735,18 @@ snapshots: jest-snapshot@29.5.0: dependencies: - '@babel/core': 7.26.9 - '@babel/generator': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.26.9) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.26.9) - '@babel/traverse': 7.27.1 - '@babel/types': 7.27.1 + '@babel/core': 7.28.0 + '@babel/generator': 7.29.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0) + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 '@jest/expect-utils': 29.5.0 '@jest/transform': 29.5.0 '@jest/types': 29.5.0 '@types/babel__traverse': 7.20.6 '@types/prettier': 2.7.3 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.26.9) + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.28.0) chalk: 4.1.2 expect: 29.5.0 graceful-fs: 4.2.11 @@ -19285,12 +20797,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)): + jest@29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3)): dependencies: - '@jest/core': 29.5.0(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + '@jest/core': 29.5.0(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3)) '@jest/types': 29.5.0 import-local: 3.1.0 - jest-cli: 29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + jest-cli: 29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - supports-color @@ -19302,6 +20814,8 @@ snapshots: jose@4.15.2: {} + jose@6.2.1: {} + joycon@3.1.1: {} js-beautify@1.15.1: @@ -19418,6 +20932,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-schema@0.4.0: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -19580,6 +21096,34 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.0 + llmz@0.0.54(@botpress/client@1.36.0)(@botpress/cognitive@0.3.15)(@bpinternal/thicktoken@2.0.0)(@bpinternal/zui@1.3.3): + dependencies: + '@babel/core': 7.28.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.0 + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.28.0) + '@babel/preset-typescript': 7.26.0(@babel/core@7.28.0) + '@babel/standalone': 7.26.4 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@botpress/cognitive': 0.3.15 + '@bpinternal/thicktoken': 2.0.0 + '@bpinternal/zui': 1.3.3 + '@jitl/quickjs-singlefile-browser-release-sync': 0.31.0 + bytes: 3.1.2 + exponential-backoff: 3.1.1 + handlebars: 4.7.8 + lodash-es: 4.17.21 + lru-cache: 11.0.2 + ms: 2.1.3 + prettier: 3.8.1 + quickjs-emscripten-core: 0.31.0 + ulid: 2.4.0 + optionalDependencies: + '@botpress/client': 1.36.0(debug@4.4.3) + transitivePeerDependencies: + - supports-color + load-tsconfig@0.2.5: {} local-ref-resolver@0.2.0: {} @@ -19693,6 +21237,8 @@ snapshots: lru_map@0.3.3: {} + luxon@3.7.2: {} + magic-string@0.30.12: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -19926,6 +21472,8 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + memoizee@0.4.15: dependencies: d: 1.0.2 @@ -19939,6 +21487,8 @@ snapshots: merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -19948,9 +21498,9 @@ snapshots: '@types/debug': 4.1.12 '@types/lodash': 4.17.0 '@types/url-join': 4.0.3 - axios: 0.21.4(debug@4.4.1) + axios: 0.21.4(debug@4.4.3) camel-case: 4.1.2 - debug: 4.4.1 + debug: 4.4.3 lodash: 4.17.21 map-obj: 4.3.0 pascal-case: 3.1.2 @@ -19965,7 +21515,7 @@ snapshots: '@types/lodash': 4.17.0 '@types/warning': 3.0.3 append-query: 2.1.1 - axios: 0.21.4(debug@4.4.1) + axios: 0.21.4(debug@4.4.3) axios-error: 1.0.4 form-data: 3.0.1 lodash: 4.17.21 @@ -20193,7 +21743,7 @@ snapshots: micromark@2.11.4: dependencies: - debug: 4.4.1 + debug: 4.4.3 parse-entities: 2.0.0 transitivePeerDependencies: - supports-color @@ -20201,7 +21751,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1 + debug: 4.4.3 decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -20232,10 +21782,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mime@2.6.0: {} @@ -20250,6 +21806,10 @@ snapshots: mimic-response@1.0.1: {} + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + minimatch@3.0.8: dependencies: brace-expansion: 1.1.11 @@ -20276,12 +21836,18 @@ snapshots: minipass@7.1.2: {} + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + mkdirp@1.0.4: {} mnemonist@0.38.3: dependencies: obliterator: 1.6.1 + module-details-from-path@1.0.4: {} + moment@2.29.4: {} moment@2.30.1: {} @@ -20318,7 +21884,7 @@ snapshots: - '@types/node' optional: true - msw@2.12.0(@types/node@22.16.4)(typescript@5.8.3): + msw@2.12.0(@types/node@22.16.4)(typescript@5.9.3): dependencies: '@inquirer/confirm': 5.1.19(@types/node@22.16.4) '@mswjs/interceptors': 0.40.0 @@ -20339,7 +21905,7 @@ snapshots: until-async: 3.0.2 yargs: 17.7.2 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.3 transitivePeerDependencies: - '@types/node' @@ -20367,6 +21933,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} next-tick@1.1.0: {} @@ -20388,6 +21956,8 @@ snapshots: node-addon-api@3.2.1: {} + node-addon-api@7.1.1: {} + node-domexception@1.0.0: {} node-fetch@2.7.0: @@ -20441,6 +22011,11 @@ snapshots: dependencies: path-key: 4.0.0 + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -20460,6 +22035,10 @@ snapshots: object-keys@1.1.1: {} + object-sizeof@2.6.5: + dependencies: + buffer: 6.0.3 + object.assign@4.1.7: dependencies: call-bind: 1.0.8 @@ -20535,14 +22114,15 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 - openai@5.12.1(ws@8.19.0)(zod@3.24.2): + openai@5.12.1(ws@8.19.0)(zod@3.25.76): optionalDependencies: ws: 8.19.0 - zod: 3.24.2 + zod: 3.25.76 - openai@6.9.0(ws@8.19.0): + openai@6.9.0(ws@8.19.0)(zod@3.25.76): optionalDependencies: ws: 8.19.0 + zod: 3.25.76 openapi-types@12.1.3: {} @@ -20605,6 +22185,10 @@ snapshots: dependencies: yocto-queue: 1.2.1 + p-limit@7.3.0: + dependencies: + yocto-queue: 1.2.1 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -20631,6 +22215,8 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + package-json@6.5.0: dependencies: got: 9.6.0 @@ -20657,11 +22243,13 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-ms@4.0.0: {} + parse-srcset@1.0.2: {} parse-statements@1.0.11: {} @@ -20701,8 +22289,14 @@ snapshots: passport-strategy@1.0.0: {} + patch-console@2.0.0: {} + + path-browserify@1.0.1: {} + path-exists@4.0.0: {} + path-expression-matcher@1.1.3: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -20723,10 +22317,17 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-scurry@2.0.2: + dependencies: + lru-cache: 11.0.2 + minipass: 7.1.2 + path-to-regexp@0.1.12: {} path-to-regexp@6.3.0: {} + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} pathe@1.1.2: {} @@ -20798,6 +22399,8 @@ snapshots: pirates@4.0.5: {} + pkce-challenge@5.0.1: {} + pkg-dir@4.2.0: dependencies: find-up: 4.1.0 @@ -20808,13 +22411,13 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)): + postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3)): dependencies: lilconfig: 3.1.3 yaml: 2.7.0 optionalDependencies: postcss: 8.4.47 - ts-node: 10.9.2(@types/node@22.16.4)(typescript@5.8.3) + ts-node: 10.9.2(@types/node@22.16.4)(typescript@5.9.3) postcss@8.4.47: dependencies: @@ -20836,6 +22439,10 @@ snapshots: dependencies: '@posthog/core': 1.6.0 + posthog-node@5.28.2: + dependencies: + '@posthog/core': 1.23.4 + preact-render-to-string@6.5.13(preact@10.26.6): dependencies: preact: 10.26.6 @@ -20854,7 +22461,9 @@ snapshots: prettier@3.4.2: {} - prettier@3.5.3: {} + prettier@3.8.1: {} + + pretty-bytes@7.1.0: {} pretty-format@29.5.0: dependencies: @@ -20862,6 +22471,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.2.0 + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + prismjs@1.29.0: {} process-nextick-args@2.0.1: {} @@ -20912,6 +22525,10 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + qs@6.5.3: {} query-string@6.14.1: @@ -20940,6 +22557,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -20953,16 +22577,29 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-dom@18.3.1(react@19.2.3): + dependencies: + loose-envify: 1.4.0 + react: 19.2.3 + scheduler: 0.23.2 + react-is@18.2.0: {} react-promise-suspense@0.3.4: dependencies: fast-deep-equal: 2.0.1 + react-reconciler@0.33.0(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.27.0 + react@18.3.1: dependencies: loose-envify: 1.4.0 + react@19.2.3: {} + readable-stream@1.1.14: dependencies: core-util-is: 1.0.2 @@ -21141,11 +22778,18 @@ snapshots: require-from-string@2.0.2: {} + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + requires-port@1.0.0: {} - resend@4.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + resend@4.6.0(react-dom@18.3.1(react@19.2.3))(react@19.2.3): dependencies: - '@react-email/render': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-email/render': 1.1.2(react-dom@18.3.1(react@19.2.3))(react@19.2.3) transitivePeerDependencies: - react - react-dom @@ -21172,6 +22816,11 @@ snapshots: dependencies: lowercase-keys: 1.0.1 + restore-cursor@4.0.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -21213,6 +22862,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.24.2 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + rrweb-cssom@0.7.1: optional: true @@ -21282,6 +22941,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + scheduler@0.27.0: {} + scmp@2.1.0: {} seek-bzip@1.0.6: @@ -21308,6 +22969,8 @@ snapshots: semver@7.7.2: {} + semver@7.7.4: {} + send@0.19.0: dependencies: debug: 2.6.9 @@ -21326,6 +22989,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serve-static@1.16.2: dependencies: encodeurl: 2.0.0 @@ -21335,6 +23014,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -21466,6 +23154,11 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.0.0 + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + snake-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -21570,6 +23263,11 @@ snapshots: get-east-asian-width: 1.3.0 strip-ansi: 7.1.0 + string-width@8.2.0: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + string.prototype.trim@1.2.10: dependencies: call-bind: 1.0.8 @@ -21616,6 +23314,10 @@ snapshots: dependencies: ansi-regex: 6.0.1 + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-bom@3.0.0: {} strip-bom@4.0.0: {} @@ -21628,6 +23330,8 @@ snapshots: strip-final-newline@3.0.0: {} + strip-final-newline@4.0.0: {} + strip-json-comments@2.0.1: {} strip-json-comments@3.1.1: {} @@ -21639,6 +23343,8 @@ snapshots: strnum@1.0.5: {} + strnum@2.2.0: {} + sucrase@3.35.0: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -21676,7 +23382,7 @@ snapshots: dependencies: component-emitter: 1.3.0 cookiejar: 2.1.4 - debug: 4.4.1 + debug: 4.4.3 fast-safe-stringify: 2.1.1 form-data: 3.0.1 formidable: 1.2.6 @@ -21692,7 +23398,7 @@ snapshots: dependencies: component-emitter: 1.3.0 cookiejar: 2.1.4 - debug: 4.4.1 + debug: 4.4.3 fast-safe-stringify: 2.1.1 form-data: 4.0.5 formidable: 2.1.2 @@ -21742,6 +23448,10 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 + system-architecture@0.1.0: {} + + tagged-tag@1.0.0: {} + tar-stream@1.6.2: dependencies: bl: 1.2.3 @@ -21752,6 +23462,14 @@ snapshots: to-buffer: 1.1.1 xtend: 4.0.2 + tar@7.5.11: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + telegraf@4.16.3: dependencies: '@telegraf/types': 7.1.0 @@ -21766,6 +23484,8 @@ snapshots: - encoding - supports-color + terminal-size@4.0.1: {} + test-exclude@6.0.0: dependencies: '@istanbuljs/schema': 0.1.3 @@ -21800,6 +23520,11 @@ snapshots: fdir: 6.4.3(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + tinypool@1.0.1: {} tinyrainbow@1.2.0: {} @@ -21888,22 +23613,27 @@ snapshots: dependencies: tslib: 1.14.1 - ts-jest@29.1.0(@babel/core@7.26.9)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.26.9))(jest@29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3): + ts-jest@29.1.0(@babel/core@7.28.0)(@jest/types@29.5.0)(babel-jest@29.5.0(@babel/core@7.28.0))(jest@29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + jest: 29.5.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3)) jest-util: 29.5.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.6.3 - typescript: 5.8.3 + typescript: 5.9.3 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.26.9 + '@babel/core': 7.28.0 '@jest/types': 29.5.0 - babel-jest: 29.5.0(@babel/core@7.26.9) + babel-jest: 29.5.0(@babel/core@7.28.0) + + ts-morph@27.0.2: + dependencies: + '@ts-morph/common': 0.28.1 + code-block-writer: 13.0.3 ts-node@10.9.2(@types/node@22.16.4)(typescript@5.6.3): dependencies: @@ -21923,7 +23653,7 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3): + ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 @@ -21937,7 +23667,7 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.8.3 + typescript: 5.9.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optional: true @@ -21963,7 +23693,7 @@ snapshots: tslib@2.6.2: {} - tsup@8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3))(typescript@5.8.3): + tsup@8.0.2(@microsoft/api-extractor@7.49.0(@types/node@22.16.4))(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3))(typescript@5.9.3): dependencies: bundle-require: 4.0.2(esbuild@0.19.12) cac: 6.7.14 @@ -21973,7 +23703,7 @@ snapshots: execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 - postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.3)) resolve-from: 5.0.0 rollup: 4.24.2 source-map: 0.8.0-beta.0 @@ -21982,7 +23712,7 @@ snapshots: optionalDependencies: '@microsoft/api-extractor': 7.49.0(@types/node@22.16.4) postcss: 8.4.47 - typescript: 5.8.3 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - ts-node @@ -22058,11 +23788,21 @@ snapshots: type-fest@4.41.0: {} + type-fest@5.4.4: + dependencies: + tagged-tag: 1.0.0 + type-is@1.6.18: dependencies: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + type@2.7.2: {} typed-array-buffer@1.0.3: @@ -22117,6 +23857,8 @@ snapshots: typescript@5.8.3: {} + typescript@5.9.3: {} + uc.micro@2.1.0: {} uglify-js@3.19.3: @@ -22126,6 +23868,8 @@ snapshots: ulid@2.4.0: {} + ulid@3.0.2: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.3 @@ -22148,6 +23892,8 @@ snapshots: unfetch@4.2.0: {} + unicorn-magic@0.3.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -22277,7 +24023,7 @@ snapshots: v8-to-istanbul@9.1.0: dependencies: - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 @@ -22337,7 +24083,7 @@ snapshots: vite-node@2.1.8(@types/node@22.16.4): dependencies: cac: 6.7.14 - debug: 4.4.1 + debug: 4.4.3 es-module-lexer: 1.6.0 pathe: 1.1.2 vite: 5.4.10(@types/node@22.16.4) @@ -22397,10 +24143,10 @@ snapshots: - supports-color - terser - vitest@2.1.8(@types/node@22.16.4)(jsdom@24.1.3)(msw@2.12.0(@types/node@22.16.4)(typescript@5.8.3)): + vitest@2.1.8(@types/node@22.16.4)(jsdom@24.1.3)(msw@2.12.0(@types/node@22.16.4)(typescript@5.9.3)): dependencies: '@vitest/expect': 2.1.8 - '@vitest/mocker': 2.1.8(msw@2.12.0(@types/node@22.16.4)(typescript@5.8.3))(vite@5.4.10(@types/node@22.16.4)) + '@vitest/mocker': 2.1.8(msw@2.12.0(@types/node@22.16.4)(typescript@5.9.3))(vite@5.4.10(@types/node@22.16.4)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.8 '@vitest/snapshot': 2.1.8 @@ -22545,6 +24291,10 @@ snapshots: dependencies: string-width: 4.2.3 + widest-line@6.0.0: + dependencies: + string-width: 8.2.0 + winston-transport@4.9.0: dependencies: logform: 2.7.0 @@ -22606,8 +24356,7 @@ snapshots: ws@8.18.2: {} - ws@8.19.0: - optional: true + ws@8.19.0: {} wsl-utils@0.1.0: dependencies: @@ -22631,6 +24380,8 @@ snapshots: yallist@4.0.0: {} + yallist@5.0.0: {} + yaml@1.10.2: {} yaml@2.7.0: {} @@ -22662,16 +24413,31 @@ snapshots: yoctocolors-cjs@2.1.3: {} + yoctocolors@2.1.2: {} + + yoga-layout@3.2.1: {} + zod-to-json-schema@3.24.6(zod@3.24.2): dependencies: zod: 3.24.2 + zod-to-json-schema@3.25.1(zod@3.24.2): + dependencies: + zod: 3.24.2 + optional: true + + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod@3.22.4: {} zod@3.23.8: {} zod@3.24.2: {} + zod@3.25.76: {} + zwitch@1.0.5: {} zwitch@2.0.4: {}