Scenario generator for vore roleplay and story ideas.
https://scenario-generator.deliciousreya.net/responses
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
209 lines
6.7 KiB
209 lines
6.7 KiB
import type { GeneratedContents, RollTableResult } from '../../common/rolltable';
|
|
import {
|
|
type FinalGeneratedContents,
|
|
type GeneratedState,
|
|
type InProgressGeneratedContents,
|
|
type RollTableResultFull
|
|
} from '../../common/rolltable';
|
|
import {
|
|
ButtonStyle,
|
|
type ComponentActionRow,
|
|
type ComponentButton,
|
|
type ComponentSelectMenu,
|
|
type ComponentSelectOption,
|
|
ComponentType,
|
|
type EmbedAuthorOptions,
|
|
EmbedField,
|
|
MessageEmbed,
|
|
type MessageEmbedOptions,
|
|
type MessageOptions
|
|
} from 'slash-create/web';
|
|
import markdownEscape from 'markdown-escape';
|
|
import type { EmbedFooterOptions } from 'slash-create/web';
|
|
|
|
export const SCENARIO_COLOR = 0x15A3C7;
|
|
export const SUCCESS_COLOR = 0x79AC78;
|
|
export const WARNING_COLOR = 0xF8ED62;
|
|
export const FAILURE_COLOR = 0xA70000;
|
|
|
|
export const LOCK_SUFFIX = ' \u{1f512}';
|
|
export const UNLOCK_SUFFIX = ' \u{1f513}';
|
|
export const ROLL_SUFFIX = ' \u{1f3b2}'
|
|
|
|
const suffixes = [[LOCK_SUFFIX, false], [UNLOCK_SUFFIX, true], [ROLL_SUFFIX, true]] as const
|
|
|
|
export function generateAuthorForResult(result: RollTableResultFull): EmbedAuthorOptions|undefined {
|
|
return result.author ? {
|
|
name: `${result.author.relation} ${result.author.name}`,
|
|
url: result.author.url ?? undefined
|
|
} : undefined;
|
|
}
|
|
|
|
export function generateFooterForResult(result: RollTableResultFull): EmbedFooterOptions {
|
|
return {
|
|
text: `in ${result.set.name ? 'the' : 'a'} ${result.set.global ? 'global' : 'server-local'} response set${result.set.name ? ' ' + markdownEscape(result.set.name) : ''}`
|
|
};
|
|
}
|
|
|
|
export function generateFieldForResult(value: RollTableResult, selected?: boolean): EmbedField {
|
|
let name = markdownEscape(`${value.table.header}${typeof selected === 'boolean' ? selected ? ROLL_SUFFIX : LOCK_SUFFIX : ''}`);
|
|
return {
|
|
name: name,
|
|
value: markdownEscape(value.text),
|
|
};
|
|
}
|
|
|
|
export function generateEmbedForResult(title: string, color: number, value: RollTableResultFull): MessageEmbedOptions {
|
|
return {
|
|
title,
|
|
color,
|
|
author: generateAuthorForResult(value),
|
|
fields: [generateFieldForResult(value)],
|
|
timestamp: value.updated,
|
|
footer: generateFooterForResult(value),
|
|
}
|
|
}
|
|
|
|
export function generateEmbedForScenario(color: number, state: GeneratedState): MessageEmbedOptions {
|
|
const fields: EmbedField[] = [];
|
|
for (const value of state.rolled.values()) {
|
|
fields.push(generateFieldForResult(value, state.final || !value.table.full ? undefined : state.selected.has(value.table)));
|
|
}
|
|
return {
|
|
title: 'Your generated scenario',
|
|
color,
|
|
fields,
|
|
timestamp: new Date()
|
|
};
|
|
}
|
|
|
|
export function getEmbedFrom({ embeds }: { embeds?: MessageEmbed[] }): MessageEmbed {
|
|
const result = embeds && embeds.length >= 1 ? embeds[0] : null;
|
|
if (!result) {
|
|
throw Error('there were no embeds on the message to read');
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function loadEmbed(embed: MessageEmbed, final: false): InProgressGeneratedContents
|
|
export function loadEmbed(embed: MessageEmbed, final: true): FinalGeneratedContents
|
|
export function loadEmbed(embed: MessageEmbed, final: boolean): GeneratedContents {
|
|
const rolled = new Map<string, string>()
|
|
const selection = new Set<string>()
|
|
if (!embed.fields) {
|
|
throw Error('there were no fields on the embed to read');
|
|
}
|
|
for (const field of embed.fields) {
|
|
let suffixInfo: readonly [string, boolean]|null = null
|
|
for (const potentialSuffixInfo of suffixes) {
|
|
if ((!suffixInfo || (potentialSuffixInfo[0].length > suffixInfo[0].length)) && field.name.endsWith(potentialSuffixInfo[0])) {
|
|
suffixInfo = potentialSuffixInfo
|
|
}
|
|
}
|
|
if (suffixInfo) {
|
|
const [suffix, selected] = suffixInfo
|
|
const name = field.name.substring(0, suffix ? field.name.length - suffix.length : undefined)
|
|
rolled.set(name, field.value)
|
|
if (selected) {
|
|
selection.add(name)
|
|
}
|
|
} else {
|
|
rolled.set(field.name, field.value)
|
|
}
|
|
}
|
|
if (final) {
|
|
return {
|
|
final,
|
|
rolled,
|
|
}
|
|
} else {
|
|
return {
|
|
final: false,
|
|
rolled,
|
|
selected: selection,
|
|
}
|
|
}
|
|
}
|
|
|
|
export const SELECT_ID = 'selected';
|
|
export const REROLL_ID = 'reroll';
|
|
export const DONE_ID = 'done';
|
|
export const DELETE_ID = 'delete';
|
|
|
|
export function generateActionsFor(state: GeneratedState): ComponentActionRow[] {
|
|
if (state.final) {
|
|
return [];
|
|
}
|
|
const selectOptions: ComponentSelectOption[] = Array.from(state.rolled.keys()).flatMap((v) => (v.full ? [{
|
|
default: state.selected.has(v),
|
|
value: v.identifier,
|
|
label: v.name,
|
|
emoji: { name: v.emoji }
|
|
}] : []));
|
|
if (selectOptions.length === 0) {
|
|
return [];
|
|
}
|
|
const select: ComponentSelectMenu = {
|
|
type: ComponentType.STRING_SELECT,
|
|
custom_id: SELECT_ID,
|
|
disabled: false,
|
|
max_values: selectOptions.length,
|
|
min_values: 0,
|
|
options: selectOptions,
|
|
placeholder: 'Components to reroll'
|
|
};
|
|
const selectRow: ComponentActionRow = { type: ComponentType.ACTION_ROW, components: [select] };
|
|
const rerollButton: ComponentButton = {
|
|
type: ComponentType.BUTTON,
|
|
custom_id: REROLL_ID,
|
|
disabled: state.selected.size === 0,
|
|
emoji: { name: '\u{1f3b2}' },
|
|
label: (state.selected.size === 0 ? 'Reroll' : state.selected.size === state.rolled.size ? 'Reroll ALL' : 'Reroll Selected'),
|
|
style: ButtonStyle.PRIMARY
|
|
};
|
|
const doneButton: ComponentButton = {
|
|
type: ComponentType.BUTTON,
|
|
custom_id: DONE_ID,
|
|
disabled: false,
|
|
emoji: { name: '\u{1f44d}' },
|
|
label: 'Looks good!',
|
|
style: ButtonStyle.SUCCESS
|
|
};
|
|
const deleteButton: ComponentButton = {
|
|
type: ComponentType.BUTTON,
|
|
custom_id: DELETE_ID,
|
|
disabled: false,
|
|
emoji: { name: '\u{1f5d1}\ufe0f' },
|
|
label: 'Trash it.',
|
|
style: ButtonStyle.DESTRUCTIVE
|
|
};
|
|
const buttonRow: ComponentActionRow = {
|
|
type: ComponentType.ACTION_ROW,
|
|
components: [rerollButton, doneButton, deleteButton]
|
|
};
|
|
return [selectRow, buttonRow];
|
|
}
|
|
|
|
export function generateMessageFor(state: GeneratedState): MessageOptions {
|
|
return { embeds: [generateEmbedForScenario(SCENARIO_COLOR, state)], components: generateActionsFor(state), ephemeral: false }
|
|
}
|
|
|
|
export function recordError<T extends {error: unknown, context?: string, extraData?: string}>(input: T): T & {message: string, stack: string} {
|
|
const {error, context, extraData} = input
|
|
const message = error instanceof Error ? error.message : `${error}`
|
|
const stack = (error instanceof Error ? error.stack : null) ?? `${error}`
|
|
console.error(`when trying to ${context ?? 'do something (unknown context)'}: ${stack}${extraData ? '\nExtra data: ' + extraData : ''}`)
|
|
return {...input, message, stack}
|
|
}
|
|
|
|
export function generateErrorMessageFor(input: {error: unknown, context?: string, title?: string, extraData?: string}): MessageOptions {
|
|
const {context, title, message} = recordError(input)
|
|
return {
|
|
embeds: [{
|
|
title: title ?? 'Error',
|
|
description: `I wasn't able to ${markdownEscape(context ?? 'do that')}. Thing is, ${markdownEscape(message)}...`,
|
|
color: FAILURE_COLOR,
|
|
}],
|
|
ephemeral: true
|
|
};
|
|
}
|
|
|