commit
05c4bae65d
@ -0,0 +1,9 @@ |
||||
root = true |
||||
|
||||
[*] |
||||
indent_style = space |
||||
indent_size = 4 |
||||
end_of_line = lf |
||||
charset = utf-8 |
||||
trim_trailing_whitespace = true |
||||
insert_final_newline = true |
@ -0,0 +1,6 @@ |
||||
DISCORD_APP_ID= |
||||
DISCORD_PUBLIC_KEY= |
||||
DISCORD_BOT_TOKEN= |
||||
|
||||
# You can fill this in for development.env |
||||
DEVELOPMENT_GUILD_ID= |
@ -0,0 +1,46 @@ |
||||
module.exports = { |
||||
env: { |
||||
commonjs: true, |
||||
es6: true, |
||||
browser: true |
||||
}, |
||||
extends: ['eslint:recommended', 'plugin:prettier/recommended'], |
||||
globals: { |
||||
DISCORD_APP_ID: true, |
||||
DISCORD_PUBLIC_KEY: true, |
||||
DISCORD_BOT_TOKEN: true |
||||
}, |
||||
parser: '@typescript-eslint/parser', |
||||
parserOptions: { |
||||
ecmaVersion: 6, |
||||
sourceType: 'module' |
||||
}, |
||||
plugins: ['@typescript-eslint'], |
||||
rules: { |
||||
'prettier/prettier': 'warn', |
||||
'no-cond-assign': [2, 'except-parens'], |
||||
'no-unused-vars': 0, |
||||
'@typescript-eslint/no-unused-vars': 1, |
||||
'no-empty': [ |
||||
'error', |
||||
{ |
||||
allowEmptyCatch: true |
||||
} |
||||
], |
||||
'prefer-const': [ |
||||
'warn', |
||||
{ |
||||
destructuring: 'all' |
||||
} |
||||
], |
||||
'spaced-comment': 'warn' |
||||
}, |
||||
overrides: [ |
||||
{ |
||||
files: ['slash-up.config.js', 'webpack.config.js'], |
||||
env: { |
||||
node: true |
||||
} |
||||
} |
||||
] |
||||
}; |
@ -0,0 +1,8 @@ |
||||
/coverage |
||||
/dist |
||||
/node_modules |
||||
wrangler.toml |
||||
/.vscode/settings.json |
||||
*.log |
||||
*.env |
||||
.dev.vars |
@ -0,0 +1,52 @@ |
||||
<component name="ProjectCodeStyleConfiguration"> |
||||
<code_scheme name="Project" version="173"> |
||||
<HTMLCodeStyleSettings> |
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" /> |
||||
<option name="HTML_QUOTE_STYLE" value="Single" /> |
||||
<option name="HTML_ENFORCE_QUOTES" value="true" /> |
||||
</HTMLCodeStyleSettings> |
||||
<JSCodeStyleSettings version="0"> |
||||
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" /> |
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" /> |
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" /> |
||||
<option name="USE_DOUBLE_QUOTES" value="false" /> |
||||
<option name="FORCE_QUOTE_STYlE" value="true" /> |
||||
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" /> |
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" /> |
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" /> |
||||
</JSCodeStyleSettings> |
||||
<TypeScriptCodeStyleSettings version="0"> |
||||
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" /> |
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" /> |
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" /> |
||||
<option name="USE_DOUBLE_QUOTES" value="false" /> |
||||
<option name="FORCE_QUOTE_STYlE" value="true" /> |
||||
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" /> |
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" /> |
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" /> |
||||
</TypeScriptCodeStyleSettings> |
||||
<VueCodeStyleSettings> |
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" /> |
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" /> |
||||
</VueCodeStyleSettings> |
||||
<codeStyleSettings language="HTML"> |
||||
<option name="SOFT_MARGINS" value="120" /> |
||||
<indentOptions> |
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" /> |
||||
</indentOptions> |
||||
</codeStyleSettings> |
||||
<codeStyleSettings language="JavaScript"> |
||||
<option name="SOFT_MARGINS" value="120" /> |
||||
</codeStyleSettings> |
||||
<codeStyleSettings language="TypeScript"> |
||||
<option name="SOFT_MARGINS" value="120" /> |
||||
</codeStyleSettings> |
||||
<codeStyleSettings language="Vue"> |
||||
<option name="SOFT_MARGINS" value="120" /> |
||||
<indentOptions> |
||||
<option name="INDENT_SIZE" value="4" /> |
||||
<option name="TAB_SIZE" value="4" /> |
||||
</indentOptions> |
||||
</codeStyleSettings> |
||||
</code_scheme> |
||||
</component> |
@ -0,0 +1,5 @@ |
||||
<component name="ProjectCodeStyleConfiguration"> |
||||
<state> |
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" /> |
||||
</state> |
||||
</component> |
@ -0,0 +1,7 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="PrettierConfiguration"> |
||||
<option name="myRunOnSave" value="true" /> |
||||
<option name="myRunOnReformat" value="true" /> |
||||
</component> |
||||
</project> |
@ -0,0 +1,116 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="AutoImportSettings"> |
||||
<option name="autoReloadType" value="SELECTIVE" /> |
||||
</component> |
||||
<component name="ChangeListManager"> |
||||
<list default="true" id="84c57704-c34b-42d3-90dc-bc3746b4a30c" name="Changes" comment=""> |
||||
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" /> |
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> |
||||
</list> |
||||
<option name="SHOW_DIALOG" value="false" /> |
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" /> |
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> |
||||
<option name="LAST_RESOLUTION" value="IGNORE" /> |
||||
</component> |
||||
<component name="FileTemplateManagerImpl"> |
||||
<option name="RECENT_TEMPLATES"> |
||||
<list> |
||||
<option value="TypeScript File" /> |
||||
</list> |
||||
</option> |
||||
</component> |
||||
<component name="Git.Settings"> |
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> |
||||
</component> |
||||
<component name="MarkdownSettingsMigration"> |
||||
<option name="stateVersion" value="1" /> |
||||
</component> |
||||
<component name="ProjectId" id="2IHAWQfFWS3VH0VkyejFrSj157w" /> |
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="true" /> |
||||
<component name="ProjectViewState"> |
||||
<option name="hideEmptyMiddlePackages" value="true" /> |
||||
<option name="showLibraryContents" value="true" /> |
||||
</component> |
||||
<component name="PropertiesComponent"><![CDATA[{ |
||||
"keyToString": { |
||||
"RunOnceActivity.OpenProjectViewOnStart": "true", |
||||
"RunOnceActivity.ShowReadmeOnStart": "true", |
||||
"WebServerToolWindowFactoryState": "false", |
||||
"last_opened_file_path": "/home/reya/WebstormProjects/steppies/node_modules/ts-jest", |
||||
"node.js.detected.package.eslint": "true", |
||||
"node.js.detected.package.standard": "true", |
||||
"node.js.detected.package.stylelint": "true", |
||||
"node.js.detected.package.tslint": "true", |
||||
"node.js.selected.package.eslint": "(autodetect)", |
||||
"node.js.selected.package.standard": "", |
||||
"node.js.selected.package.stylelint": "", |
||||
"node.js.selected.package.tslint": "(autodetect)", |
||||
"nodejs.jest.jest_package": "/home/reya/WebstormProjects/steppies/node_modules/jest", |
||||
"nodejs_interpreter_path": "node", |
||||
"nodejs_package_manager_path": "npm", |
||||
"prettierjs.PrettierConfiguration.Package": "/home/reya/WebstormProjects/steppies/node_modules/prettier", |
||||
"settings.editor.selected.configurable": "configurable.group.appearance", |
||||
"ts.external.directory.path": "/opt/WebStorm/plugins/JavaScriptLanguage/jsLanguageServicesImpl/external", |
||||
"vue.rearranger.settings.migration": "true" |
||||
} |
||||
}]]></component> |
||||
<component name="RunManager" selected="Jest.All Tests"> |
||||
<configuration name="All Tests" type="JavaScriptTestRunnerJest" nameIsGenerated="true"> |
||||
<node-interpreter value="project" /> |
||||
<node-options value="" /> |
||||
<jest-package value="$PROJECT_DIR$/node_modules/jest" /> |
||||
<working-dir value="$PROJECT_DIR$" /> |
||||
<envs /> |
||||
<scope-kind value="ALL" /> |
||||
<method v="2" /> |
||||
</configuration> |
||||
<configuration name="Debug Application" type="JavascriptDebugType" uri="http://localhost:3000"> |
||||
<method v="2" /> |
||||
</configuration> |
||||
<configuration name="npm start" type="js.build_tools.npm"> |
||||
<package-json value="$PROJECT_DIR$/package.json" /> |
||||
<command value="run" /> |
||||
<scripts> |
||||
<script value="start" /> |
||||
</scripts> |
||||
<node-interpreter value="project" /> |
||||
<envs /> |
||||
<method v="2" /> |
||||
</configuration> |
||||
<list> |
||||
<item itemvalue="JavaScript Debug.Debug Application" /> |
||||
<item itemvalue="Jest.All Tests" /> |
||||
<item itemvalue="npm.npm start" /> |
||||
</list> |
||||
</component> |
||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" /> |
||||
<component name="TaskManager"> |
||||
<task active="true" id="Default" summary="Default task"> |
||||
<changelist id="84c57704-c34b-42d3-90dc-bc3746b4a30c" name="Changes" comment="" /> |
||||
<created>1669828165304</created> |
||||
<option name="number" value="Default" /> |
||||
<option name="presentableId" value="Default" /> |
||||
<updated>1669828165304</updated> |
||||
<workItem from="1669828166439" duration="20904000" /> |
||||
</task> |
||||
<servers /> |
||||
</component> |
||||
<component name="TypeScriptGeneratedFilesManager"> |
||||
<option name="version" value="3" /> |
||||
</component> |
||||
<component name="Vcs.Log.Tabs.Properties"> |
||||
<option name="TAB_STATES"> |
||||
<map> |
||||
<entry key="MAIN"> |
||||
<value> |
||||
<State /> |
||||
</value> |
||||
</entry> |
||||
</map> |
||||
</option> |
||||
</component> |
||||
<component name="com.intellij.coverage.CoverageDataManagerImpl"> |
||||
<SUITE FILE_PATH="coverage/steppies$All_Tests.info" NAME="All Tests Coverage Results" MODIFIED="1669853649183" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="JestJavaScriptTestRunnerCoverage" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" /> |
||||
</component> |
||||
</project> |
@ -0,0 +1,8 @@ |
||||
{ |
||||
"semi": false, |
||||
"singleQuote": true, |
||||
"tabWidth": 4, |
||||
"useTabs": false, |
||||
"trailingComma": "all", |
||||
"printWidth": 120 |
||||
} |
@ -0,0 +1,8 @@ |
||||
{ |
||||
"recommendations": [ |
||||
"deepscan.vscode-deepscan", |
||||
"dbaeumer.vscode-eslint", |
||||
"esbenp.prettier-vscode", |
||||
"editorconfig.editorconfig" |
||||
] |
||||
} |
@ -0,0 +1,51 @@ |
||||
# /create with Cloudflare Workers |
||||
|
||||
A [slash-create](https://npm.im/slash-create) template, using [Cloudflare Workers](https://workers.cloudflare.com). |
||||
|
||||
[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/Snazzah/slash-create-worker) |
||||
|
||||
## Getting Started |
||||
### Cloning the repo |
||||
You can either use degit to locally clone this repo without git, or [create a new repo from this template](https://github.com/Snazzah/slash-create-worker/generate) and clone that. |
||||
```sh |
||||
npx degit Snazzah/slash-create-worker |
||||
``` |
||||
|
||||
After that, make sure to install dependencies using npm or yarn: |
||||
```sh |
||||
npm install |
||||
# yarn |
||||
``` |
||||
### Installing and setting up Wrangler |
||||
> Make sure to [sign up for a Cloudflare Workers account](https://dash.cloudflare.com/sign-up/workers) in a browser before continuing. |
||||
Install wrangler with npm or yarn: |
||||
```sh |
||||
npm install -D wrangler@latest |
||||
# yarn global add wrangler@latest |
||||
``` |
||||
Read more about [installing wrangler](https://developers.cloudflare.com/workers/cli-wrangler/install-update). |
||||
|
||||
Afterwards, run `wrangler login` to login to your Cloudflare account with OAuth: |
||||
```sh |
||||
wrangler login |
||||
``` |
||||
|
||||
Copy `wrangler.example.toml` into `wrangler.toml`. Make sure to fill in your account ID in the config and update the name of the worker. You can find your account ID [here](https://dash.cloudflare.com/?to=/:account/workers) towards the right side. |
||||
|
||||
### Filling in secrets |
||||
You can enter in environment secrets with `wrangler secret put`, here are the keys that are required to run this: |
||||
```sh |
||||
npx wrangler secret put DISCORD_APP_ID |
||||
npx wrangler secret put DISCORD_PUBLIC_KEY |
||||
npx wrangler secret put DISCORD_BOT_TOKEN |
||||
``` |
||||
|
||||
### Development |
||||
To run this locally, copy `.env.example` to `.dev.vars` and fill in the variables, then you can run `npm run dev` (or `yarn dev`) to start a local dev environment and use something like ngrok to tunnel it to a URL. |
||||
|
||||
To sync commands in the development environment, copy `.env.example` to `development.env` and fill in the variables, then run `npm run sync:dev` (or `yarn sync:dev`). |
||||
|
||||
> Note: When you create a command, make sure to include it in the array of commands in `./src/commands/index.ts`. |
||||
|
||||
### Production |
||||
To sync to production, copy `.env.example` to `.env` and fill in the variables, then run `npm run sync`. To publish code to a worker, run `npm run deploy`. |
@ -0,0 +1,6 @@ |
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */ |
||||
module.exports = { |
||||
preset: 'ts-jest', |
||||
testEnvironment: 'node', |
||||
collectCoverageFrom: ['src/**/*.{ts,js}', '!src/index.ts', '!src/types.d.ts', '!src/shim/**/*.{ts,js}'], |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,41 @@ |
||||
{ |
||||
"name": "temptress-bot", |
||||
"description": "Bot for horny thoughts", |
||||
"author": "Reya C.", |
||||
"main": "dist/worker.js", |
||||
"private": true, |
||||
"scripts": { |
||||
"build": "webpack", |
||||
"dev": "wrangler dev -l", |
||||
"deploy": "wrangler publish", |
||||
"sync": "slash-up sync", |
||||
"sync:dev": "slash-up sync -e development", |
||||
"lint": "eslint --ext .ts .", |
||||
"lint:fix": "eslint --ext .ts . --fix", |
||||
"test": "jest", |
||||
"postinstall": "patch-package" |
||||
}, |
||||
"devDependencies": { |
||||
"@cloudflare/workers-types": "^2.0.0", |
||||
"@jest/globals": "^29.3.1", |
||||
"@typescript-eslint/eslint-plugin": "^4.31.2", |
||||
"@typescript-eslint/parser": "^4.31.2", |
||||
"dotenv": "^10.0.0", |
||||
"eslint": "^7.32.0", |
||||
"eslint-config-prettier": "^8.3.0", |
||||
"eslint-plugin-prettier": "^4.0.0", |
||||
"prettier": "^2.0.5", |
||||
"rimraf": "^3.0.2", |
||||
"slash-create": "^5.5.2", |
||||
"slash-up": "^1.1.2", |
||||
"ts-jest": "^29.0.3", |
||||
"ts-loader": "^7.0.5", |
||||
"typescript": "^4.4.3", |
||||
"webpack": "^5.75.0", |
||||
"webpack-cli": "^5.0.0", |
||||
"wrangler": "^2.0.5" |
||||
}, |
||||
"dependencies": { |
||||
"patch-package": "^6.4.7" |
||||
} |
||||
} |
@ -0,0 +1,13 @@ |
||||
diff --git a/node_modules/slash-create/lib/structures/interfaces/messageInteraction.d.ts b/node_modules/slash-create/lib/structures/interfaces/messageInteraction.d.ts
|
||||
index 9ecfd7f..70ec168 100644
|
||||
--- a/node_modules/slash-create/lib/structures/interfaces/messageInteraction.d.ts
|
||||
+++ b/node_modules/slash-create/lib/structures/interfaces/messageInteraction.d.ts
|
||||
@@ -153,7 +153,7 @@ export interface EditMessageOptions {
|
||||
/** A file within {@link EditMessageOptions}. */
|
||||
export interface MessageFile {
|
||||
/** The attachment to send. */
|
||||
- file: Buffer;
|
||||
+ file: Blob | Uint8Array | File;
|
||||
/** The name of the file. */
|
||||
name: string;
|
||||
}
|
@ -0,0 +1,19 @@ |
||||
// This is the slash-up config file.
|
||||
// Make sure to fill in "token" and "applicationId" before using.
|
||||
// You can also use environment variables from the ".env" file if any.
|
||||
|
||||
module.exports = { |
||||
// The Token of the Discord bot
|
||||
token: process.env.DISCORD_BOT_TOKEN, |
||||
// The Application ID of the Discord bot
|
||||
applicationId: process.env.DISCORD_APP_ID, |
||||
// This is where the path to command files are, .ts files are supported!
|
||||
commandPath: './src/commands', |
||||
// You can use different environments with --env (-e)
|
||||
env: { |
||||
development: { |
||||
// The "globalToGuild" option makes global commands sync to the specified guild instead.
|
||||
globalToGuild: process.env.DEVELOPMENT_GUILD_ID |
||||
} |
||||
} |
||||
}; |
@ -0,0 +1,25 @@ |
||||
import { SlashCommand, CommandOptionType, SlashCreator, CommandContext } from 'slash-create'; |
||||
|
||||
export default class BotCommand extends SlashCommand { |
||||
constructor(creator: SlashCreator) { |
||||
super(creator, { |
||||
name: 'hello', |
||||
description: 'Says hello to you.', |
||||
options: [ |
||||
{ |
||||
type: CommandOptionType.STRING, |
||||
name: 'food', |
||||
description: 'What food do you like?' |
||||
} |
||||
] |
||||
}); |
||||
|
||||
creator.registerGlobalComponent("hi", (interact) => { |
||||
|
||||
}) |
||||
} |
||||
|
||||
async run(ctx: CommandContext) { |
||||
return ctx.options.food ? `You like ${ctx.options.food}? Nice!` : `Hello, ${ctx.user.username}!`; |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
export const commands = [require('./hello')]; |
@ -0,0 +1,19 @@ |
||||
import { commands } from './commands' |
||||
import { SlashCreator, CFWorkerServer } from './shim' |
||||
|
||||
export const creator = new SlashCreator({ |
||||
applicationID: DISCORD_APP_ID, |
||||
publicKey: DISCORD_PUBLIC_KEY, |
||||
token: DISCORD_BOT_TOKEN, |
||||
}) |
||||
|
||||
creator.withServer(new CFWorkerServer()).registerCommands(commands) |
||||
|
||||
creator.on('warn', (message) => console.warn(message)) |
||||
creator.on('error', (error) => console.error(error.stack || error.toString())) |
||||
creator.on('commandRun', (command, _, ctx) => |
||||
console.info(`${ctx.user.username}#${ctx.user.discriminator} (${ctx.user.id}) ran command ${command.commandName}`), |
||||
) |
||||
creator.on('commandError', (command, error) => |
||||
console.error(`Command ${command.commandName} errored:`, error.stack || error.toString()), |
||||
) |
@ -0,0 +1,73 @@ |
||||
import { |
||||
SlashCreator as Creator, |
||||
RespondFunction, |
||||
Server, |
||||
SlashCreatorOptions, |
||||
TransformedRequest |
||||
} from 'slash-create'; |
||||
import { FetchRequestHandler } from './util/requestHandler'; |
||||
import { verify } from './util/verify'; |
||||
|
||||
// @ts-expect-error doesn't like _onRequest
|
||||
export class SlashCreator extends Creator { |
||||
/** The request handler for the creator */ |
||||
readonly requestHandler: FetchRequestHandler; |
||||
|
||||
/** @param opts The options for the creator */ |
||||
constructor(opts: SlashCreatorOptions) { |
||||
// eslint-disable-next-line constructor-super
|
||||
super(opts); |
||||
// @ts-ignore
|
||||
this.requestHandler = new FetchRequestHandler(this); |
||||
} |
||||
|
||||
/** |
||||
* Attaches a server to the creator. |
||||
* @param server The server to use |
||||
*/ |
||||
withServer(server: Server) { |
||||
if (this.server) throw new Error('A server was already set in this creator.'); |
||||
this.server = server; |
||||
|
||||
if (this.server.isWebserver) { |
||||
if (!this.options.publicKey) throw new Error('A public key is required to be set when using a webserver.'); |
||||
// @ts-ignore
|
||||
this.server.createEndpoint(this.options.endpointPath as string, this._onRequest.bind(this)); |
||||
// @ts-ignore
|
||||
} else this.server.handleInteraction((interaction) => this._onInteraction(interaction, null, false)); |
||||
|
||||
return this; |
||||
} |
||||
|
||||
// Overwriting the verification method to use Web Crypto API and use waitUntil for the promise
|
||||
private async _onRequest(treq: TransformedRequest, respond: RespondFunction, wait: (f: any) => void) { |
||||
this.emit('debug', 'Got request'); |
||||
|
||||
// Verify request
|
||||
const signature = treq.headers['x-signature-ed25519'] as string; |
||||
const timestamp = treq.headers['x-signature-timestamp'] as string; |
||||
|
||||
// Check if both signature and timestamp exists, and the timestamp isn't past due.
|
||||
if ( |
||||
!signature || |
||||
!timestamp || |
||||
parseInt(timestamp) < (Date.now() - (this.options.maxSignatureTimestamp as number)) / 1000 |
||||
) |
||||
return respond({ |
||||
status: 401, |
||||
body: 'Invalid signature' |
||||
}); |
||||
|
||||
if (!(await verify(treq))) { |
||||
this.emit('debug', 'A request failed to be verified'); |
||||
this.emit('unverifiedRequest', treq); |
||||
return respond({ |
||||
status: 401, |
||||
body: 'Invalid signature' |
||||
}); |
||||
} |
||||
|
||||
// @ts-expect-error
|
||||
wait(this._onInteraction(treq.body, respond, true).catch(() => {})); |
||||
} |
||||
} |
@ -0,0 +1,4 @@ |
||||
export * from './creator'; |
||||
export * from './servers/cfworker'; |
||||
export * from './util/requestHandler'; |
||||
export * from './util/verify'; |
@ -0,0 +1,63 @@ |
||||
import { RespondFunction, Server, TransformedRequest } from 'slash-create'; |
||||
import { MultipartData } from '../util/multipartData'; |
||||
|
||||
export type ServerRequestHandler = (treq: TransformedRequest, respond: RespondFunction, wait: (f: any) => void) => void; |
||||
|
||||
/** |
||||
* A server for Cloudflare Workers. |
||||
* @see https://developers.cloudflare.com/workers/
|
||||
*/ |
||||
export class CFWorkerServer extends Server { |
||||
constructor() { |
||||
super({ alreadyListening: true }); |
||||
this.isWebserver = true; |
||||
} |
||||
|
||||
/** @private */ |
||||
createEndpoint(path: string, handler: ServerRequestHandler) { |
||||
addEventListener('fetch', (event) => { |
||||
if (event.request.method !== 'POST') |
||||
return event.respondWith(new Response('Server only supports POST requests.', { status: 405 })); |
||||
return event.respondWith( |
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
new Promise(async (resolve) => { |
||||
const body = await event.request.text(); |
||||
handler( |
||||
{ |
||||
headers: Object.fromEntries(event.request.headers.entries()), |
||||
body: body ? JSON.parse(body) : body, |
||||
request: event.request, |
||||
response: null |
||||
}, |
||||
async (response) => { |
||||
if (response.files) { |
||||
const data = new MultipartData(); |
||||
for (const file of response.files) await data.attach(file.name, file.file, file.name); |
||||
await data.attach('payload_json', JSON.stringify(response.body)); |
||||
resolve( |
||||
new Response(data.finish(), { |
||||
status: response.status || 200, |
||||
headers: { |
||||
...((response.headers || {}) as Record<string, string>), |
||||
'content-type': 'multipart/form-data; boundary=' + data.boundary |
||||
} |
||||
}) |
||||
); |
||||
} else |
||||
resolve( |
||||
new Response(JSON.stringify(response.body), { |
||||
status: response.status || 200, |
||||
headers: { |
||||
...((response.headers || {}) as Record<string, string>), |
||||
'content-type': 'application/json' |
||||
} |
||||
}) |
||||
); |
||||
}, |
||||
event.waitUntil.bind(event) |
||||
); |
||||
}) |
||||
); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,42 @@ |
||||
export class MultipartData { |
||||
boundary = '----------------SlashCreate'; |
||||
bufs: Uint8Array[] = []; |
||||
|
||||
async attach(fieldName: string, data: any, filename?: string) { |
||||
if (data === undefined) return; |
||||
let str = '\r\n--' + this.boundary + '\r\nContent-Disposition: form-data; name="' + fieldName + '"'; |
||||
if (filename) str += '; filename="' + filename + '"'; |
||||
if (data instanceof Blob || data instanceof File) { |
||||
str += `\r\nContent-Type: ${data.type}`; |
||||
data = new Uint8Array(await data.arrayBuffer()); |
||||
} else if (data instanceof ArrayBuffer || data instanceof Uint8Array) { |
||||
str += '\r\nContent-Type: application/octet-stream'; |
||||
data = new Uint8Array(data); |
||||
} else if (typeof data === 'object') { |
||||
str += '\r\nContent-Type: application/json'; |
||||
data = encode(JSON.stringify(data)); |
||||
} else { |
||||
data = encode('' + data); |
||||
} |
||||
|
||||
this.bufs.push(encode(str + '\r\n\r\n')); |
||||
this.bufs.push(data); |
||||
} |
||||
|
||||
finish() { |
||||
this.bufs.push(encode('\r\n--' + this.boundary + '--')); |
||||
|
||||
let index = 0; |
||||
const result = new Uint8Array(this.bufs.reduce((a, b) => a + b.byteLength, 0)); |
||||
for (const buf of this.bufs) { |
||||
result.set(new Uint8Array(buf), index); |
||||
index += buf.byteLength; |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
|
||||
function encode(text: string) { |
||||
return new TextEncoder().encode(text); |
||||
} |
@ -0,0 +1,61 @@ |
||||
import { RequestHandler } from 'slash-create'; |
||||
import { MultipartData } from './multipartData'; |
||||
|
||||
/** |
||||
* The request handler for REST requests. |
||||
* @private |
||||
*/ |
||||
export class FetchRequestHandler extends RequestHandler { |
||||
/** |
||||
* Make an API request |
||||
* @param method Uppercase HTTP method |
||||
* @param url URL of the endpoint |
||||
* @param auth Whether to add the Authorization header and token or not |
||||
* @param body Request payload |
||||
* @param file The file(s) to send |
||||
*/ |
||||
async request(method: string, url: string, auth = true, body?: any, file?: any): Promise<any> { |
||||
// @ts-ignore
|
||||
const creator = this._creator; |
||||
const headers: Record<string, string> = { |
||||
'User-Agent': this.userAgent, |
||||
'Accept-Encoding': 'gzip,deflate', |
||||
'X-RateLimit-Precision': 'millisecond' |
||||
}; |
||||
let data: any = body; |
||||
|
||||
if (auth) { |
||||
if (!creator.options.token) throw new Error('No token was set in the SlashCreator.'); |
||||
headers.Authorization = creator.options.token; |
||||
} |
||||
|
||||
if (file) { |
||||
if (Array.isArray(file) || file.file) { |
||||
data = new MultipartData(); |
||||
headers['Content-Type'] = 'multipart/form-data; boundary=' + data.boundary; |
||||
if (Array.isArray(file)) for (const f of file) await (data as MultipartData).attach(f.name, f.file, f.name); |
||||
else await (data as MultipartData).attach(file.name, file.file, file.name); |
||||
if (body) await (data as MultipartData).attach('payload_json', JSON.stringify(body)); |
||||
data = data.finish(); |
||||
} else throw new Error('Invalid file object'); |
||||
} else if (body) { |
||||
if (method !== 'GET' && method !== 'DELETE') { |
||||
data = JSON.stringify(body); |
||||
headers['Content-Type'] = 'application/json'; |
||||
} |
||||
} |
||||
|
||||
const res = await fetch('https://discord.com' + this.baseURL + url, { method, body: data, headers }); |
||||
|
||||
if (!res.ok) { |
||||
const data = await res.text(); |
||||
throw new Error(`${method} got ${res.status} - ${data}`); |
||||
} |
||||
|
||||
return await res.json(); |
||||
} |
||||
|
||||
toString() { |
||||
return '[RequestHandler]'; |
||||
} |
||||
} |
@ -0,0 +1,40 @@ |
||||
// from https://gist.github.com/devsnek/77275f6e3f810a9545440931ed314dc1
|
||||
|
||||
import { TransformedRequest } from 'slash-create'; |
||||
|
||||
function hex2bin(hex: string) { |
||||
const buf = new Uint8Array(Math.ceil(hex.length / 2)); |
||||
for (var i = 0; i < buf.length; i++) { |
||||
buf[i] = parseInt(hex.substr(i * 2, 2), 16); |
||||
} |
||||
return buf; |
||||
} |
||||
|
||||
const encoder = new TextEncoder(); |
||||
|
||||
let publicKey: CryptoKey; |
||||
async function getPublicKey() { |
||||
if (publicKey) return publicKey; |
||||
// @ts-expect-error Node.js needs to know this is a public key
|
||||
publicKey = await crypto.subtle.importKey( |
||||
'raw', |
||||
hex2bin(DISCORD_PUBLIC_KEY), |
||||
{ name: 'NODE-ED25519', namedCurve: 'NODE-ED25519', public: true }, |
||||
true, |
||||
['verify'] |
||||
); |
||||
return publicKey; |
||||
} |
||||
|
||||
export async function verify(treq: TransformedRequest) { |
||||
const signature = hex2bin(treq.headers['x-signature-ed25519'] as string); |
||||
const timestamp = treq.headers['x-signature-timestamp'] as string; |
||||
const unknown = JSON.stringify(treq.body); |
||||
|
||||
return await crypto.subtle.verify( |
||||
'NODE-ED25519', |
||||
await getPublicKey(), |
||||
signature, |
||||
encoder.encode(timestamp + unknown) |
||||
); |
||||
} |
@ -0,0 +1,62 @@ |
||||
import { describe, expect, test } from '@jest/globals' |
||||
import { DieState, isHeldState, isSelectedState, setDeselected, setSelected, toggleSelected } from './gamestate' |
||||
|
||||
describe('isHeldState', () => { |
||||
test.each<[DieState, boolean]>([ |
||||
[DieState.ROLLED, false], |
||||
[DieState.SELECTED, false], |
||||
[DieState.HELD, true], |
||||
[DieState.HELD_SELECTED, true], |
||||
[DieState.SCORED, false], |
||||
])('(%p) returns %p', (state, value): void => { |
||||
expect(isHeldState(state)).toEqual(value) |
||||
}) |
||||
}) |
||||
|
||||
describe('isSelectedState', () => { |
||||
test.each<[DieState, boolean]>([ |
||||
[DieState.ROLLED, false], |
||||
[DieState.SELECTED, true], |
||||
[DieState.HELD, false], |
||||
[DieState.HELD_SELECTED, true], |
||||
[DieState.SCORED, false], |
||||
])('(%p) returns %p', (state, value): void => { |
||||
expect(isSelectedState(state)).toEqual(value) |
||||
}) |
||||
}) |
||||
|
||||
describe('setSelected', () => { |
||||
test.each<[DieState, DieState]>([ |
||||
[DieState.ROLLED, DieState.SELECTED], |
||||
[DieState.SELECTED, DieState.SELECTED], |
||||
[DieState.HELD, DieState.HELD_SELECTED], |
||||
[DieState.HELD_SELECTED, DieState.HELD_SELECTED], |
||||
[DieState.SCORED, DieState.SCORED], |
||||
])('(%p) returns %p', (state, value): void => { |
||||
expect(setSelected(state)).toEqual(value) |
||||
}) |
||||
}) |
||||
|
||||
describe('setDeselected', () => { |
||||
test.each<[DieState, DieState]>([ |
||||
[DieState.ROLLED, DieState.ROLLED], |
||||
[DieState.SELECTED, DieState.ROLLED], |
||||
[DieState.HELD, DieState.HELD], |
||||
[DieState.HELD_SELECTED, DieState.HELD], |
||||
[DieState.SCORED, DieState.SCORED], |
||||
])('(%p) returns %p', (state, value): void => { |
||||
expect(setDeselected(state)).toEqual(value) |
||||
}) |
||||
}) |
||||
|
||||
describe('toggleSelected', () => { |
||||
test.each<[DieState, DieState]>([ |
||||
[DieState.ROLLED, DieState.SELECTED], |
||||
[DieState.SELECTED, DieState.ROLLED], |
||||
[DieState.HELD, DieState.HELD_SELECTED], |
||||
[DieState.HELD_SELECTED, DieState.HELD], |
||||
[DieState.SCORED, DieState.SCORED], |
||||
])('(%p) returns %p', (state, value): void => { |
||||
expect(toggleSelected(state)).toEqual(value) |
||||
}) |
||||
}) |
@ -0,0 +1,339 @@ |
||||
export enum DieFace { |
||||
FAIL = 'F', |
||||
STOP = 'S', |
||||
SCORE_0 = '0', |
||||
SCORE_1 = '1', |
||||
SCORE_2 = '2', |
||||
SCORE_3 = '3', |
||||
SCORE_4 = '4', |
||||
SCORE_5 = '5', |
||||
WILD = '*', |
||||
HEART = '<3', |
||||
QUESTION = '?', |
||||
} |
||||
|
||||
export enum DieState { |
||||
ROLLED = 'rolled', |
||||
SELECTED = 'selected', |
||||
HELD = 'held', |
||||
HELD_SELECTED = 'held_selected', |
||||
SCORED = 'scored', |
||||
} |
||||
|
||||
export function isHeldState(d: DieState): d is DieState.HELD | DieState.HELD_SELECTED { |
||||
return d === DieState.HELD || d === DieState.HELD_SELECTED |
||||
} |
||||
|
||||
export function isSelectedState(d: DieState): d is DieState.SELECTED | DieState.HELD_SELECTED { |
||||
return d === DieState.SELECTED || d === DieState.HELD_SELECTED |
||||
} |
||||
|
||||
export function setSelected(d: DieState): DieState.SELECTED | DieState.HELD_SELECTED | DieState.SCORED { |
||||
switch (d) { |
||||
case DieState.ROLLED: |
||||
return DieState.SELECTED |
||||
case DieState.HELD: |
||||
return DieState.HELD_SELECTED |
||||
case DieState.SELECTED: |
||||
case DieState.HELD_SELECTED: |
||||
case DieState.SCORED: |
||||
return d |
||||
} |
||||
} |
||||
|
||||
export function setDeselected(d: DieState): DieState.ROLLED | DieState.HELD | DieState.SCORED { |
||||
switch (d) { |
||||
case DieState.SELECTED: |
||||
return DieState.ROLLED |
||||
case DieState.HELD_SELECTED: |
||||
return DieState.HELD |
||||
case DieState.ROLLED: |
||||
case DieState.HELD: |
||||
case DieState.SCORED: |
||||
return d |
||||
} |
||||
} |
||||
|
||||
export function toggleSelected(d: DieState): DieState { |
||||
switch (d) { |
||||
case DieState.ROLLED: |
||||
return DieState.SELECTED |
||||
case DieState.SELECTED: |
||||
return DieState.ROLLED |
||||
case DieState.HELD: |
||||
return DieState.HELD_SELECTED |
||||
case DieState.HELD_SELECTED: |
||||
return DieState.HELD |
||||
case DieState.SCORED: |
||||
return DieState.SCORED |
||||
} |
||||
} |
||||
|
||||
export interface DieResult { |
||||
readonly type: DieType |
||||
readonly face: DieFace |
||||
readonly state: DieState |
||||
} |
||||
|
||||
export interface DieType { |
||||
// The name of this die type
|
||||
readonly name: string |
||||
// The faces of this die type; faces may be repeated to increase their odds
|
||||
readonly faces: readonly DieFace[] |
||||
} |
||||
|
||||
export interface AIWeights { |
||||
// How much this AI likes to choose actions that increase the damage bonus
|
||||
// damageBonusWeight is static and does not change
|
||||
readonly damageBonusWeight: number |
||||
// How much this AI likes to choose actions that increase the requirement of stop dice in a turn
|
||||
// stopWeight is multiplied by the number of stop dice both players can gain
|
||||
readonly stopWeight: number |
||||
// How much this AI likes to choose actions that decrease the limit of fail dice in a turn
|
||||
// failWeight is multiplied by the number of fail dice both players can lose
|
||||
readonly failWeight: number |
||||
// How much this AI likes to choose actions that are finishers for the opponent.
|
||||
// finisherWeight is multiplied by the percentage of max damage the opponent has taken
|
||||
readonly finisherWeight: number |
||||
// How much this AI dislikes choosing actions that are finishers for itself.
|
||||
// selfFinisherWeight is multiplied by the percentage of max damage the AI has taken
|
||||
// If this would reduce the weight of an action to 0 or less, that action has a weight of 0 and will only be
|
||||
// selected randomly if all actions have a weight of 0.
|
||||
readonly selfFinisherWeight: number |
||||
// Approximate lead that the AI would prefer to have before ending its turn when it has the option to keep pressing
|
||||
readonly desiredLead: number |
||||
// Approximate damage behind the previous turn total that the AI is willing to take to play it safe,
|
||||
// assuming doing so wouldn't be fatal (not enough damage to kill or not a finisher)
|
||||
// When there's one fail die left, the AI will prioritize stopping as long as the damage it will be taking is within
|
||||
// a randomly selected 50-150% of this value
|
||||
readonly allowedDamage: number |
||||
// Integer from 0-100:
|
||||
// 100 means AI keeps rolling until it has one fail die left before trying to stop
|
||||
// 0 means AI always stops as soon as it has at least the desired lead
|
||||
// Anything in between is the percentage chance of continuing, which is rolled once for each fail die remaining
|
||||
// besides the last. If the chance fails once, then the AI uses that turn as if it were trying to stop.
|
||||
// This roll is only performed when the AI already has the lead, is ahead by a value within an acceptable distance
|
||||
// of the desired lead, and the AI has at least two additional fail dice remaining before failing.
|
||||
readonly recklessnessPercent: number |
||||
// Integer from 0-100:
|
||||
// 0 means AI never holds stops until it's ready to end its turn
|
||||
// 100 means AI always holds stops it sees
|
||||
// Anything in between is the percentage chance of locking a new stop die that appears, rolled once per stop die
|
||||
// needed to end the turn per die that comes up stop. If the chance succeeds once, then the AI holds that stop die.
|
||||
// This roll is only performed when the AI is not already stopping its turn.
|
||||
readonly holdStopsPercent: number |
||||
} |
||||
|
||||
export interface PlayerStartingState extends Pick<PlayerState, 'damageMax' | 'stopCount' | 'failCount'> { |
||||
// Percentage of damage that this player starts off dealing
|
||||
readonly damageBonusBase: number |
||||
// Percentage of damage this player's damage increases by with each damage bonus earned
|
||||
readonly damageBonusIncrement: number |
||||
// Maximum number of damage bonuses that this player can have, or 0 if there's no upper limit
|
||||
readonly maxDamageBonuses: number |
||||
|
||||
// Minimum number of dice required to stop
|
||||
readonly minStopCount: number |
||||
// Minimum number of dice required to fail
|
||||
readonly minFailCount: number |
||||
|
||||
// Maximum number of dice required to stop
|
||||
readonly maxStopCount: number |
||||
// Maximum number of dice required to fail
|
||||
readonly maxFailCount: number |
||||
|
||||
// Base amount of damage that must be dealt to recover, or 0 if recovery is forbidden
|
||||
// Defaults to 1000
|
||||
readonly recoverBase?: number |
||||
// Amount by which the amount needed to recover increases each time recovery is earned
|
||||
// Defaults to 1000
|
||||
readonly recoverIncrement?: number |
||||
// Percentage of current damage taken that is removed when recovery is earned
|
||||
// Defaults to 50
|
||||
readonly recoverPercent?: number |
||||
|
||||
// Instructions for if this player is an AI player. If not specified, this player cannot be played by the AI.
|
||||
readonly aiData?: AIWeights |
||||
} |
||||
|
||||
export interface Difficulty { |
||||
// Name of the difficulty in the select menu
|
||||
readonly name: string |
||||
// Short description of the difficulty in the select menu
|
||||
readonly shortDescription?: string |
||||
// Description of the difficulty when selected
|
||||
readonly description?: string |
||||
|
||||
// Starting values for the top
|
||||
readonly topStats: PlayerStartingState |
||||
// Starting values for the bottom
|
||||
readonly bottomStats: PlayerStartingState |
||||
} |
||||
|
||||
export interface PlayerText { |
||||
// Possible random names for this player
|
||||
readonly names?: readonly string[] |
||||
|
||||
// Text given at the start of the game from this side
|
||||
readonly startText?: TriggeredText |
||||
// Text given at the end of the game from this side
|
||||
readonly endText?: TriggeredText |
||||
|
||||
// The name of the damage value for this player
|
||||
readonly damage: string |
||||
// The name of the player's damage limit, defaulting to "Max " + damage
|
||||
readonly maxDamage?: string |
||||
// The name of this player's stop count requirement
|
||||
readonly stopCount: string |
||||
// The name of this player's fail count limit
|
||||
readonly failCount: string |
||||
// The name of this player's incoming damage bonus
|
||||
readonly damageBonus: string |
||||
} |
||||
|
||||
export interface GameTheme { |
||||
readonly name: string |
||||
readonly shortDescription: string |
||||
readonly description: string |
||||
|
||||
readonly actions: readonly GameAction[] |
||||
|
||||
readonly difficulties: readonly Difficulty[] |
||||
|
||||
readonly topText: PlayerText |
||||
readonly bottomText: PlayerText |
||||
} |
||||
|
||||
export interface TriggeredText { |
||||
// The dialogues that can be triggered for this action.
|
||||
// Dialogue can be disabled for characters run by human players.
|
||||
readonly dialogue?: readonly string[] |
||||
// The descriptions that can be triggered for this action.
|
||||
// Descriptions are always displayed, regardless of player.
|
||||
readonly description?: readonly string[] |
||||
} |
||||
|
||||
export interface ActionText { |
||||
// Name of the action in the select list for this side
|
||||
readonly name: string |
||||
// Description of the action in the select list for this side
|
||||
readonly shortDescription: string |
||||
// Description of the action when selected for this side
|
||||
readonly description: string |
||||
|
||||
// Text given when this side selects this action.
|
||||
readonly selectAction?: TriggeredText |
||||
// Text given when the other side selects this action.
|
||||
readonly opponentSelectsAction?: TriggeredText |
||||
// Text given when this side finishes a turn in this action without failing.
|
||||
readonly passTurn?: TriggeredText |
||||
// Text given when this side receives a turn from the opponent in this action.
|
||||
readonly opponentPassesTurn?: TriggeredText |
||||
// Text given when this side rerolls the dice for this action.
|
||||
readonly reroll?: TriggeredText |
||||
// Text given when the opposing side rerolls the dice for this action.
|
||||
readonly opponentRerolls?: TriggeredText |
||||
// Text given the first time each turn this side is one fail die away from failing
|
||||
readonly aboutToFailTurn?: TriggeredText |
||||
// Text given the first time each turn the opposing side is one fail die away from failing
|
||||
readonly opponentAboutToFailTurn?: TriggeredText |
||||
// Text given when this side fails the turn by accumulating fail dice
|
||||
readonly failTurn?: TriggeredText |
||||
// Text given when the opposing side fails the turn by accumulating fail dice
|
||||
readonly opponentFailsTurn?: TriggeredText |
||||
// Text given when this side fails the turn by stopping without enough points
|
||||
readonly abandonTurn?: TriggeredText |
||||
// Text given when the opposing side fails the turn by stopping without enough points
|
||||
readonly opponentAbandonsTurn?: TriggeredText |
||||
// Text given when this side is defeated by being pushed over their max damage with this action
|
||||
readonly defeated?: TriggeredText |
||||
// Text given when this side defeats the opponent by pushing them over their max damage with this action
|
||||
readonly opponentDefeated?: TriggeredText |
||||
} |
||||
|
||||
export interface GameAction { |
||||
// The text from the top's perspective, and for the top's actions
|
||||
readonly topText: ActionText |
||||
// The text from the bottom's perspective, and for the bottom's actions
|
||||
readonly bottomText: ActionText |
||||
|
||||
// The dice that are rolled for this action
|
||||
readonly dice: readonly DieType[] |
||||
// True if the loser of this action increases the amount of damage they take
|
||||
readonly givesDamageBonus: boolean |
||||
// True if the loser of this action has to get an additional stop die to end their turn
|
||||
readonly givesStopCount: boolean |
||||
// True if the loser of this action has one fewer buffer for fail dice
|
||||
readonly givesFailCount: boolean |
||||
// Whether this action can end the game in a loss for the top
|
||||
readonly canFinishTop: boolean |
||||
// Whether this action can end the game in a loss for the bottom
|
||||
readonly canFinishBottom: boolean |
||||
} |
||||
|
||||
export interface PlayerState { |
||||
// This player's name
|
||||
readonly name: string |
||||
|
||||
// Amount of damage taken so far this battle
|
||||
readonly damage: number |
||||
// Max damage that can be taken, 0 for endless mode
|
||||
readonly damageMax: number |
||||
// The total damage taken across this battle
|
||||
readonly damageTotal: number |
||||
|
||||
// Number of stop dice required to end a turn with stops
|
||||
readonly stopCount: number |
||||
// Number of fail dice required to fail a turn by accumulating too many fail dice
|
||||
readonly failCount: number |
||||
// Incoming damage for this side is multiplied by 100% + 10% * damageBonuses
|
||||
readonly damageBonuses: number |
||||
|
||||
// Amount that Total Damage must reach to reduce this player's damage by 50% of its current value
|
||||
// 0 means no recovery is allowed for this player
|
||||
readonly nextRecoverAt: number |
||||
// Number of times damage was recovered so far this game (adds 1000 + 100 * timesRecovered to nextRecoverAt)
|
||||
readonly timesRecovered: number |
||||
} |
||||
|
||||
export interface GameState { |
||||
// The version of the serialization format this state was saved with.
|
||||
readonly version: number |
||||
|
||||
// The state of the top player
|
||||
readonly top: PlayerState |
||||
// The state of the bottom player
|
||||
readonly bottom: PlayerState |
||||
|
||||
// The theme that defines this game for the players.
|
||||
readonly theme: GameTheme |
||||
// The difficulty selected for this game.
|
||||
readonly difficulty: Difficulty |
||||
|
||||
// Null if the top is a computer.
|
||||
readonly topHumanId: string | null |
||||
// Null if the bottom is a computer.
|
||||
readonly bottomHumanId: string | null |
||||
|
||||
// If true, the top is choosing the action this round.
|
||||
readonly isTopRound: boolean |
||||
// The action chosen for this round, or null if the current player is choosing an action
|
||||
readonly action: GameAction | null |
||||
|
||||
// If true, the top is rolling this turn.
|
||||
readonly isTopTurn: boolean |
||||
// The total for the previous turn, or 0 if the current player is taking the first turn
|
||||
// If the current player fails when the turn total is 0, they take the penalty and pass the initiative without taking damage.
|
||||
// This can still end the game if the current action is capable of ending the game for this player.
|
||||
readonly lastTurnTotal: number |
||||
// The dice available for the current turn, or null if the current player has not rolled yet this turn.
|
||||
readonly lastRoll: readonly DieResult[] | null |
||||
// The total value of the selected dice, or 0 if the selected dice cannot be scored.
|
||||
readonly selectedDiceValue: number |
||||
// Whether the selected dice are sufficient to end the turn.
|
||||
readonly selectedDiceEndTurn: boolean |
||||
// The number of dice that have come up failures so far in the current turn
|
||||
readonly countedFails: number |
||||
// The total of the dice that have been scored so far this turn
|
||||
readonly currentTurnTotal: number |
||||
} |
@ -0,0 +1,5 @@ |
||||
// secrets: wrangler secret put <name>
|
||||
declare const DISCORD_APP_ID: string |
||||
declare const DISCORD_PUBLIC_KEY: string |
||||
declare const DISCORD_BOT_TOKEN: string |
||||
declare const DEVELOPMENT_GUILD_ID: string |
@ -0,0 +1,37 @@ |
||||
Activity pairs: |
||||
|
||||
Sniff foot / Press foot to nose |
||||
Lick sole / Put sole against mouth |
||||
Suck toes / Put toes in mouth |
||||
Prostrate / Debase |
||||
Squirm / Humiliate |
||||
Plead / Force to Beg |
||||
Masturbate with toes / Toe footjob |
||||
Fuck soles / Sole footjob |
||||
Grind against heel / Heel footjob |
||||
|
||||
In addition to having different dice (different sets of faces, thus different odds, plus different numbers of dice, ranging from 5-8) for each activity, the activity groups have different penalties: |
||||
|
||||
Sniff/Lick/Suck = builds intoxication/trance but cannot end the game (taking too much damage leaves the affected party over the limit but isn't a finisher) |
||||
Prostrate/Squirm/Plead = builds submissiveness/vulnerability but cannot end the game (taking too much damage leaves the affected party over the limit but isn't a finisher) |
||||
Masturbate/Fuck/Grind = builds arousal/thrill penalty multiplier for failed round; if a player is over 100% Arousal/Thrill after losing a round in one of these, that player loses the game |
||||
|
||||
Intoxication/Trance = number of Stop dice that must be stored up at once to end turn successfully (counts up from 1 to 5) |
||||
Submissiveness/Vulnerability = number of Fail dice that need to appear during a turn to end turn in failure (counts down from 5 to 1) |
||||
Arousal/Thrill Penalty Multiplier = amount of damage dealt by a failed round (counts up from 100% in 10% increments) |
||||
|
||||
Each round, turns go back and forth with each turn needing to end with a higher score than the previous turn in that round; first person to end their turn in failure or be unable to succeed enough to beat their opponent's previous turn score loses and takes the punishment for that category as well as an amount of arousal/thrill damage equal to the difference between their opponent's previous turn score and their current turn score (or 0, if they lost by accumulating Fail dice) |
||||
|
||||
Players alternate each round, with the current player for a round choosing an activity and being the first to take their turn for that activity |
||||
|
||||
Stop dice can be banked, which reduces the number of dice available but makes stopping safer (especially after building up Intoxication/Trance) |
||||
Fail dice are counted each time they appear, and do not reduce the number of dice available |
||||
Scoring dice are worth their face value each time they appear, and do not reduce the number of dice available |
||||
Combo dice can be banked and are only valuable if the full combo shows up, which scores the big points for the combo and frees the banked dice to be rolled again |
||||
|
||||
Intoxication/Trance and Submissiveness/Vulnerability decrease by 1 (minimum 1 each) after each round when at least (current value + 2) rounds have passed since their last increase |
||||
Arousal/Thrill Penalty Multiplier does not decay |
||||
|
||||
In endless mode, Thrill instead functions as a scoring mechanism, with Arousal decreasing by 50% of its current value (max 50%) every 1000 Thrill points + 100 Thrill points per Arousal decrease this game |
||||
|
||||
Arousal damage taken is also tracked, with total Arousal damage taken added to Thrill damage dealt for the final score |
@ -0,0 +1,24 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"outDir": "./dist", |
||||
"module": "commonjs", |
||||
"target": "es2020", |
||||
"lib": ["es2020", "webworker"], |
||||
"alwaysStrict": true, |
||||
"strict": true, |
||||
"preserveConstEnums": true, |
||||
"moduleResolution": "node", |
||||
"sourceMap": true, |
||||
"esModuleInterop": true, |
||||
"types": ["@cloudflare/workers-types"], |
||||
"resolveJsonModule": true |
||||
}, |
||||
"include": [ |
||||
"./src/*.ts", |
||||
"./test/*.ts", |
||||
"./src/**/*.ts", |
||||
"./test/**/*.ts", |
||||
"./node_modules/@cloudflare/workers-types/index.d.ts" |
||||
], |
||||
"exclude": ["node_modules/", "dist/"] |
||||
} |
@ -0,0 +1,32 @@ |
||||
const path = require('path'); |
||||
|
||||
module.exports = { |
||||
output: { |
||||
filename: 'worker.js', |
||||
path: path.join(__dirname, 'dist') |
||||
}, |
||||
mode: 'production', |
||||
resolve: { |
||||
extensions: ['.ts', '.tsx', '.js'], |
||||
plugins: [], |
||||
fallback: { |
||||
zlib: false, |
||||
https: false, |
||||
fs: false, |
||||
fastify: false, |
||||
express: false, |
||||
path: false |
||||
} |
||||
}, |
||||
module: { |
||||
rules: [ |
||||
{ |
||||
test: /\.tsx?$/, |
||||
loader: 'ts-loader', |
||||
options: { |
||||
transpileOnly: true |
||||
} |
||||
} |
||||
] |
||||
} |
||||
}; |
@ -0,0 +1,10 @@ |
||||
name = "slash-create-worker" |
||||
account_id = "" |
||||
main = "dist/worker.js" |
||||
compatibility_date = "2021-12-20" |
||||
|
||||
[dev] |
||||
port = 8020 |
||||
|
||||
[build] |
||||
command = "npm run build" |
Loading…
Reference in new issue